1 //===-- runtime/unit.cpp --------------------------------------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include "unit.h"
10 #include "environment.h"
11 #include "io-error.h"
12 #include "lock.h"
13 #include "unit-map.h"
14 #include <cstdio>
15 #include <limits>
16 #include <utility>
17 
18 namespace Fortran::runtime::io {
19 
20 // The per-unit data structures are created on demand so that Fortran I/O
21 // should work without a Fortran main program.
22 static Lock unitMapLock;
23 static UnitMap *unitMap{nullptr};
24 static ExternalFileUnit *defaultInput{nullptr};
25 static ExternalFileUnit *defaultOutput{nullptr};
26 
27 void FlushOutputOnCrash(const Terminator &terminator) {
28   if (!defaultOutput) {
29     return;
30   }
31   CriticalSection critical{unitMapLock};
32   if (defaultOutput) {
33     IoErrorHandler handler{terminator};
34     handler.HasIoStat(); // prevent nested crash if flush has error
35     defaultOutput->FlushOutput(handler);
36   }
37 }
38 
39 ExternalFileUnit *ExternalFileUnit::LookUp(int unit) {
40   return GetUnitMap().LookUp(unit);
41 }
42 
43 ExternalFileUnit &ExternalFileUnit::LookUpOrCrash(
44     int unit, const Terminator &terminator) {
45   ExternalFileUnit *file{LookUp(unit)};
46   if (!file) {
47     terminator.Crash("Not an open I/O unit number: %d", unit);
48   }
49   return *file;
50 }
51 
52 ExternalFileUnit &ExternalFileUnit::LookUpOrCreate(
53     int unit, const Terminator &terminator, bool &wasExtant) {
54   return GetUnitMap().LookUpOrCreate(unit, terminator, wasExtant);
55 }
56 
57 ExternalFileUnit &ExternalFileUnit::LookUpOrCreateAnonymous(int unit,
58     Direction dir, std::optional<bool> isUnformatted,
59     const Terminator &terminator) {
60   bool exists{false};
61   ExternalFileUnit &result{
62       GetUnitMap().LookUpOrCreate(unit, terminator, exists)};
63   if (!exists) {
64     IoErrorHandler handler{terminator};
65     result.OpenAnonymousUnit(
66         dir == Direction::Input ? OpenStatus::Unknown : OpenStatus::Replace,
67         Action::ReadWrite, Position::Rewind, Convert::Native, handler);
68     result.isUnformatted = isUnformatted;
69   }
70   return result;
71 }
72 
73 ExternalFileUnit *ExternalFileUnit::LookUp(const char *path) {
74   return GetUnitMap().LookUp(path);
75 }
76 
77 ExternalFileUnit &ExternalFileUnit::CreateNew(
78     int unit, const Terminator &terminator) {
79   bool wasExtant{false};
80   ExternalFileUnit &result{
81       GetUnitMap().LookUpOrCreate(unit, terminator, wasExtant)};
82   RUNTIME_CHECK(terminator, !wasExtant);
83   return result;
84 }
85 
86 ExternalFileUnit *ExternalFileUnit::LookUpForClose(int unit) {
87   return GetUnitMap().LookUpForClose(unit);
88 }
89 
90 ExternalFileUnit &ExternalFileUnit::NewUnit(
91     const Terminator &terminator, bool forChildIo) {
92   ExternalFileUnit &unit{GetUnitMap().NewUnit(terminator)};
93   unit.createdForInternalChildIo_ = forChildIo;
94   return unit;
95 }
96 
97 void ExternalFileUnit::OpenUnit(std::optional<OpenStatus> status,
98     std::optional<Action> action, Position position, OwningPtr<char> &&newPath,
99     std::size_t newPathLength, Convert convert, IoErrorHandler &handler) {
100   if (executionEnvironment.conversion != Convert::Unknown) {
101     convert = executionEnvironment.conversion;
102   }
103   swapEndianness_ = convert == Convert::Swap ||
104       (convert == Convert::LittleEndian && !isHostLittleEndian) ||
105       (convert == Convert::BigEndian && isHostLittleEndian);
106   if (IsOpen()) {
107     bool isSamePath{newPath.get() && path() && pathLength() == newPathLength &&
108         std::memcmp(path(), newPath.get(), newPathLength) == 0};
109     if (status && *status != OpenStatus::Old && isSamePath) {
110       handler.SignalError("OPEN statement for connected unit may not have "
111                           "explicit STATUS= other than 'OLD'");
112       return;
113     }
114     if (!newPath.get() || isSamePath) {
115       // OPEN of existing unit, STATUS='OLD' or unspecified, not new FILE=
116       newPath.reset();
117       return;
118     }
119     // Otherwise, OPEN on open unit with new FILE= implies CLOSE
120     DoImpliedEndfile(handler);
121     FlushOutput(handler);
122     Close(CloseStatus::Keep, handler);
123   }
124   set_path(std::move(newPath), newPathLength);
125   Open(status.value_or(OpenStatus::Unknown), action, position, handler);
126   auto totalBytes{knownSize()};
127   if (access == Access::Direct) {
128     if (!isFixedRecordLength || !recordLength) {
129       handler.SignalError(IostatOpenBadRecl,
130           "OPEN(UNIT=%d,ACCESS='DIRECT'): record length is not known",
131           unitNumber());
132     } else if (*recordLength <= 0) {
133       handler.SignalError(IostatOpenBadRecl,
134           "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is invalid",
135           unitNumber(), static_cast<std::intmax_t>(*recordLength));
136     } else if (totalBytes && (*totalBytes % *recordLength != 0)) {
137       handler.SignalError(IostatOpenBadAppend,
138           "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is not an "
139           "even divisor of the file size %jd",
140           unitNumber(), static_cast<std::intmax_t>(*recordLength),
141           static_cast<std::intmax_t>(*totalBytes));
142     }
143   }
144   endfileRecordNumber.reset();
145   currentRecordNumber = 1;
146   if (totalBytes && recordLength && *recordLength) {
147     endfileRecordNumber = 1 + (*totalBytes / *recordLength);
148   }
149   if (position == Position::Append) {
150     if (!endfileRecordNumber) {
151       // Fake it so that we can backspace relative from the end
152       endfileRecordNumber = std::numeric_limits<std::int64_t>::max() - 2;
153     }
154     currentRecordNumber = *endfileRecordNumber;
155   }
156 }
157 
158 void ExternalFileUnit::OpenAnonymousUnit(std::optional<OpenStatus> status,
159     std::optional<Action> action, Position position, Convert convert,
160     IoErrorHandler &handler) {
161   // I/O to an unconnected unit reads/creates a local file, e.g. fort.7
162   std::size_t pathMaxLen{32};
163   auto path{SizedNew<char>{handler}(pathMaxLen)};
164   std::snprintf(path.get(), pathMaxLen, "fort.%d", unitNumber_);
165   OpenUnit(status, action, position, std::move(path), std::strlen(path.get()),
166       convert, handler);
167 }
168 
169 void ExternalFileUnit::CloseUnit(CloseStatus status, IoErrorHandler &handler) {
170   DoImpliedEndfile(handler);
171   FlushOutput(handler);
172   Close(status, handler);
173 }
174 
175 void ExternalFileUnit::DestroyClosed() {
176   GetUnitMap().DestroyClosed(*this); // destroys *this
177 }
178 
179 bool ExternalFileUnit::SetDirection(
180     Direction direction, IoErrorHandler &handler) {
181   if (direction == Direction::Input) {
182     if (mayRead()) {
183       direction_ = Direction::Input;
184       return true;
185     } else {
186       handler.SignalError(IostatReadFromWriteOnly,
187           "READ(UNIT=%d) with ACTION='WRITE'", unitNumber());
188       return false;
189     }
190   } else {
191     if (mayWrite()) {
192       direction_ = Direction::Output;
193       return true;
194     } else {
195       handler.SignalError(IostatWriteToReadOnly,
196           "WRITE(UNIT=%d) with ACTION='READ'", unitNumber());
197       return false;
198     }
199   }
200 }
201 
202 UnitMap &ExternalFileUnit::GetUnitMap() {
203   if (unitMap) {
204     return *unitMap;
205   }
206   CriticalSection critical{unitMapLock};
207   if (unitMap) {
208     return *unitMap;
209   }
210   Terminator terminator{__FILE__, __LINE__};
211   IoErrorHandler handler{terminator};
212   UnitMap *newUnitMap{New<UnitMap>{terminator}().release()};
213   bool wasExtant{false};
214   ExternalFileUnit &out{newUnitMap->LookUpOrCreate(6, terminator, wasExtant)};
215   RUNTIME_CHECK(terminator, !wasExtant);
216   out.Predefine(1);
217   out.SetDirection(Direction::Output, handler);
218   defaultOutput = &out;
219   ExternalFileUnit &in{newUnitMap->LookUpOrCreate(5, terminator, wasExtant)};
220   RUNTIME_CHECK(terminator, !wasExtant);
221   in.Predefine(0);
222   in.SetDirection(Direction::Input, handler);
223   defaultInput = &in;
224   // TODO: Set UTF-8 mode from the environment
225   unitMap = newUnitMap;
226   return *unitMap;
227 }
228 
229 void ExternalFileUnit::CloseAll(IoErrorHandler &handler) {
230   CriticalSection critical{unitMapLock};
231   if (unitMap) {
232     unitMap->CloseAll(handler);
233     FreeMemoryAndNullify(unitMap);
234   }
235   defaultOutput = nullptr;
236 }
237 
238 void ExternalFileUnit::FlushAll(IoErrorHandler &handler) {
239   CriticalSection critical{unitMapLock};
240   if (unitMap) {
241     unitMap->FlushAll(handler);
242   }
243 }
244 
245 static void SwapEndianness(
246     char *data, std::size_t bytes, std::size_t elementBytes) {
247   if (elementBytes > 1) {
248     auto half{elementBytes >> 1};
249     for (std::size_t j{0}; j + elementBytes <= bytes; j += elementBytes) {
250       for (std::size_t k{0}; k < half; ++k) {
251         std::swap(data[j + k], data[j + elementBytes - 1 - k]);
252       }
253     }
254   }
255 }
256 
257 bool ExternalFileUnit::Emit(const char *data, std::size_t bytes,
258     std::size_t elementBytes, IoErrorHandler &handler) {
259   auto furthestAfter{std::max(furthestPositionInRecord,
260       positionInRecord + static_cast<std::int64_t>(bytes))};
261   if (recordLength) {
262     // It is possible for recordLength to have a value now for a
263     // variable-length output record if the previous operation
264     // was a BACKSPACE.
265     if (!isFixedRecordLength) {
266       recordLength.reset();
267     } else if (furthestAfter > *recordLength) {
268       handler.SignalError(IostatRecordWriteOverrun,
269           "Attempt to write %zd bytes to position %jd in a fixed-size record "
270           "of %jd bytes",
271           bytes, static_cast<std::intmax_t>(positionInRecord),
272           static_cast<std::intmax_t>(*recordLength));
273       return false;
274     }
275   }
276   WriteFrame(frameOffsetInFile_, recordOffsetInFrame_ + furthestAfter, handler);
277   if (positionInRecord > furthestPositionInRecord) {
278     std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord, ' ',
279         positionInRecord - furthestPositionInRecord);
280   }
281   char *to{Frame() + recordOffsetInFrame_ + positionInRecord};
282   std::memcpy(to, data, bytes);
283   if (swapEndianness_) {
284     SwapEndianness(to, bytes, elementBytes);
285   }
286   positionInRecord += bytes;
287   furthestPositionInRecord = furthestAfter;
288   return true;
289 }
290 
291 bool ExternalFileUnit::Receive(char *data, std::size_t bytes,
292     std::size_t elementBytes, IoErrorHandler &handler) {
293   RUNTIME_CHECK(handler, direction_ == Direction::Input);
294   auto furthestAfter{std::max(furthestPositionInRecord,
295       positionInRecord + static_cast<std::int64_t>(bytes))};
296   if (furthestAfter > recordLength.value_or(furthestAfter)) {
297     handler.SignalError(IostatRecordReadOverrun,
298         "Attempt to read %zd bytes at position %jd in a record of %jd bytes",
299         bytes, static_cast<std::intmax_t>(positionInRecord),
300         static_cast<std::intmax_t>(*recordLength));
301     return false;
302   }
303   auto need{recordOffsetInFrame_ + furthestAfter};
304   auto got{ReadFrame(frameOffsetInFile_, need, handler)};
305   if (got >= need) {
306     std::memcpy(data, Frame() + recordOffsetInFrame_ + positionInRecord, bytes);
307     if (swapEndianness_) {
308       SwapEndianness(data, bytes, elementBytes);
309     }
310     positionInRecord += bytes;
311     furthestPositionInRecord = furthestAfter;
312     return true;
313   } else {
314     // EOF or error: can be handled & has been signaled
315     endfileRecordNumber = currentRecordNumber;
316     return false;
317   }
318 }
319 
320 std::size_t ExternalFileUnit::GetNextInputBytes(
321     const char *&p, IoErrorHandler &handler) {
322   RUNTIME_CHECK(handler, direction_ == Direction::Input);
323   p = FrameNextInput(handler, 1);
324   return p ? recordLength.value_or(positionInRecord + 1) - positionInRecord : 0;
325 }
326 
327 std::optional<char32_t> ExternalFileUnit::GetCurrentChar(
328     IoErrorHandler &handler) {
329   const char *p{nullptr};
330   std::size_t bytes{GetNextInputBytes(p, handler)};
331   if (bytes == 0) {
332     return std::nullopt;
333   } else {
334     // TODO: UTF-8 decoding; may have to get more bytes in a loop
335     return *p;
336   }
337 }
338 
339 const char *ExternalFileUnit::FrameNextInput(
340     IoErrorHandler &handler, std::size_t bytes) {
341   RUNTIME_CHECK(handler, isUnformatted.has_value() && !*isUnformatted);
342   if (static_cast<std::int64_t>(positionInRecord + bytes) <=
343       recordLength.value_or(positionInRecord + bytes)) {
344     auto at{recordOffsetInFrame_ + positionInRecord};
345     auto need{static_cast<std::size_t>(at + bytes)};
346     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
347     SetSequentialVariableFormattedRecordLength();
348     if (got >= need) {
349       return Frame() + at;
350     }
351     handler.SignalEnd();
352     endfileRecordNumber = currentRecordNumber;
353   }
354   return nullptr;
355 }
356 
357 bool ExternalFileUnit::SetSequentialVariableFormattedRecordLength() {
358   if (recordLength || access != Access::Sequential) {
359     return true;
360   } else if (FrameLength() > recordOffsetInFrame_) {
361     const char *record{Frame() + recordOffsetInFrame_};
362     std::size_t bytes{FrameLength() - recordOffsetInFrame_};
363     if (const char *nl{
364             reinterpret_cast<const char *>(std::memchr(record, '\n', bytes))}) {
365       recordLength = nl - record;
366       if (*recordLength > 0 && record[*recordLength - 1] == '\r') {
367         --*recordLength;
368       }
369       return true;
370     }
371   }
372   return false;
373 }
374 
375 void ExternalFileUnit::SetLeftTabLimit() {
376   leftTabLimit = furthestPositionInRecord;
377   positionInRecord = furthestPositionInRecord;
378 }
379 
380 bool ExternalFileUnit::BeginReadingRecord(IoErrorHandler &handler) {
381   RUNTIME_CHECK(handler, direction_ == Direction::Input);
382   if (!beganReadingRecord_) {
383     beganReadingRecord_ = true;
384     if (access == Access::Sequential) {
385       if (endfileRecordNumber && currentRecordNumber >= *endfileRecordNumber) {
386         handler.SignalEnd();
387       } else if (isFixedRecordLength && access == Access::Direct) {
388         RUNTIME_CHECK(handler, recordLength.has_value());
389         auto need{
390             static_cast<std::size_t>(recordOffsetInFrame_ + *recordLength)};
391         auto got{ReadFrame(frameOffsetInFile_, need, handler)};
392         if (got < need) {
393           handler.SignalEnd();
394         }
395       } else {
396         RUNTIME_CHECK(handler, isUnformatted.has_value());
397         if (isUnformatted.value_or(false)) {
398           BeginSequentialVariableUnformattedInputRecord(handler);
399         } else { // formatted
400           BeginSequentialVariableFormattedInputRecord(handler);
401         }
402       }
403     }
404   }
405   RUNTIME_CHECK(handler,
406       access != Access::Sequential || recordLength.has_value() ||
407           handler.InError());
408   return !handler.InError();
409 }
410 
411 void ExternalFileUnit::FinishReadingRecord(IoErrorHandler &handler) {
412   RUNTIME_CHECK(handler, direction_ == Direction::Input && beganReadingRecord_);
413   beganReadingRecord_ = false;
414   if (handler.InError() && handler.GetIoStat() != IostatEor) {
415     // avoid bogus crashes in END/ERR circumstances
416   } else if (access == Access::Sequential) {
417     RUNTIME_CHECK(handler, recordLength.has_value());
418     recordOffsetInFrame_ += *recordLength;
419     if (isFixedRecordLength && access == Access::Direct) {
420       frameOffsetInFile_ += recordOffsetInFrame_;
421       recordOffsetInFrame_ = 0;
422     } else {
423       RUNTIME_CHECK(handler, isUnformatted.has_value());
424       recordLength.reset();
425       if (isUnformatted.value_or(false)) {
426         // Retain footer in frame for more efficient BACKSPACE
427         frameOffsetInFile_ += recordOffsetInFrame_;
428         recordOffsetInFrame_ = sizeof(std::uint32_t);
429       } else { // formatted
430         if (FrameLength() > recordOffsetInFrame_ &&
431             Frame()[recordOffsetInFrame_] == '\r') {
432           ++recordOffsetInFrame_;
433         }
434         if (FrameLength() >= recordOffsetInFrame_ &&
435             Frame()[recordOffsetInFrame_] == '\n') {
436           ++recordOffsetInFrame_;
437         }
438         if (!pinnedFrame || mayPosition()) {
439           frameOffsetInFile_ += recordOffsetInFrame_;
440           recordOffsetInFrame_ = 0;
441         }
442       }
443     }
444   }
445   ++currentRecordNumber;
446   BeginRecord();
447 }
448 
449 bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) {
450   if (direction_ == Direction::Input) {
451     FinishReadingRecord(handler);
452     return BeginReadingRecord(handler);
453   } else { // Direction::Output
454     bool ok{true};
455     RUNTIME_CHECK(handler, isUnformatted.has_value());
456     if (isFixedRecordLength && recordLength &&
457         furthestPositionInRecord < *recordLength) {
458       // Pad remainder of fixed length record
459       WriteFrame(
460           frameOffsetInFile_, recordOffsetInFrame_ + *recordLength, handler);
461       std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord,
462           isUnformatted.value_or(false) ? 0 : ' ',
463           *recordLength - furthestPositionInRecord);
464       furthestPositionInRecord = *recordLength;
465     }
466     if (!(isFixedRecordLength && access == Access::Direct)) {
467       positionInRecord = furthestPositionInRecord;
468       if (isUnformatted.value_or(false)) {
469         // Append the length of a sequential unformatted variable-length record
470         // as its footer, then overwrite the reserved first four bytes of the
471         // record with its length as its header.  These four bytes were skipped
472         // over in BeginUnformattedIO<Output>().
473         // TODO: Break very large records up into subrecords with negative
474         // headers &/or footers
475         std::uint32_t length;
476         length = furthestPositionInRecord - sizeof length;
477         ok = ok &&
478             Emit(reinterpret_cast<const char *>(&length), sizeof length,
479                 sizeof length, handler);
480         positionInRecord = 0;
481         ok = ok &&
482             Emit(reinterpret_cast<const char *>(&length), sizeof length,
483                 sizeof length, handler);
484       } else {
485         // Terminate formatted variable length record
486         ok = ok && Emit("\n", 1, 1, handler); // TODO: Windows CR+LF
487       }
488     }
489     CommitWrites();
490     impliedEndfile_ = true;
491     ++currentRecordNumber;
492     if (endfileRecordNumber && currentRecordNumber >= *endfileRecordNumber) {
493       endfileRecordNumber.reset();
494     }
495     return ok;
496   }
497 }
498 
499 void ExternalFileUnit::BackspaceRecord(IoErrorHandler &handler) {
500   if (access != Access::Sequential) {
501     handler.SignalError(IostatBackspaceNonSequential,
502         "BACKSPACE(UNIT=%d) on non-sequential file", unitNumber());
503   } else {
504     if (endfileRecordNumber && currentRecordNumber > *endfileRecordNumber) {
505       // BACKSPACE after explicit ENDFILE
506       currentRecordNumber = *endfileRecordNumber;
507     } else {
508       DoImpliedEndfile(handler);
509       if (frameOffsetInFile_ + recordOffsetInFrame_ > 0) {
510         --currentRecordNumber;
511         if (isFixedRecordLength && access == Access::Direct) {
512           BackspaceFixedRecord(handler);
513         } else {
514           RUNTIME_CHECK(handler, isUnformatted.has_value());
515           if (isUnformatted.value_or(false)) {
516             BackspaceVariableUnformattedRecord(handler);
517           } else {
518             BackspaceVariableFormattedRecord(handler);
519           }
520         }
521       }
522     }
523     BeginRecord();
524   }
525 }
526 
527 void ExternalFileUnit::FlushOutput(IoErrorHandler &handler) {
528   if (!mayPosition()) {
529     auto frameAt{FrameAt()};
530     if (frameOffsetInFile_ >= frameAt &&
531         frameOffsetInFile_ <
532             static_cast<std::int64_t>(frameAt + FrameLength())) {
533       // A Flush() that's about to happen to a non-positionable file
534       // needs to advance frameOffsetInFile_ to prevent attempts at
535       // impossible seeks
536       CommitWrites();
537     }
538   }
539   Flush(handler);
540 }
541 
542 void ExternalFileUnit::FlushIfTerminal(IoErrorHandler &handler) {
543   if (isTerminal()) {
544     FlushOutput(handler);
545   }
546 }
547 
548 void ExternalFileUnit::Endfile(IoErrorHandler &handler) {
549   if (access != Access::Sequential) {
550     handler.SignalError(IostatEndfileNonSequential,
551         "ENDFILE(UNIT=%d) on non-sequential file", unitNumber());
552   } else if (!mayWrite()) {
553     handler.SignalError(IostatEndfileUnwritable,
554         "ENDFILE(UNIT=%d) on read-only file", unitNumber());
555   } else if (endfileRecordNumber &&
556       currentRecordNumber > *endfileRecordNumber) {
557     // ENDFILE after ENDFILE
558   } else {
559     DoEndfile(handler);
560     // Explicit ENDFILE leaves position *after* the endfile record
561     RUNTIME_CHECK(handler, endfileRecordNumber.has_value());
562     currentRecordNumber = *endfileRecordNumber + 1;
563   }
564 }
565 
566 void ExternalFileUnit::Rewind(IoErrorHandler &handler) {
567   if (access == Access::Direct) {
568     handler.SignalError(IostatRewindNonSequential,
569         "REWIND(UNIT=%d) on non-sequential file", unitNumber());
570   } else {
571     DoImpliedEndfile(handler);
572     SetPosition(0);
573     currentRecordNumber = 1;
574   }
575 }
576 
577 void ExternalFileUnit::EndIoStatement() {
578   io_.reset();
579   u_.emplace<std::monostate>();
580   lock_.Drop();
581 }
582 
583 void ExternalFileUnit::BeginSequentialVariableUnformattedInputRecord(
584     IoErrorHandler &handler) {
585   std::int32_t header{0}, footer{0};
586   std::size_t need{recordOffsetInFrame_ + sizeof header};
587   std::size_t got{ReadFrame(frameOffsetInFile_, need, handler)};
588   // Try to emit informative errors to help debug corrupted files.
589   const char *error{nullptr};
590   if (got < need) {
591     if (got == recordOffsetInFrame_) {
592       handler.SignalEnd();
593     } else {
594       error = "Unformatted variable-length sequential file input failed at "
595               "record #%jd (file offset %jd): truncated record header";
596     }
597   } else {
598     std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header);
599     recordLength = sizeof header + header; // does not include footer
600     need = recordOffsetInFrame_ + *recordLength + sizeof footer;
601     got = ReadFrame(frameOffsetInFile_, need, handler);
602     if (got < need) {
603       error = "Unformatted variable-length sequential file input failed at "
604               "record #%jd (file offset %jd): hit EOF reading record with "
605               "length %jd bytes";
606     } else {
607       std::memcpy(&footer, Frame() + recordOffsetInFrame_ + *recordLength,
608           sizeof footer);
609       if (footer != header) {
610         error = "Unformatted variable-length sequential file input failed at "
611                 "record #%jd (file offset %jd): record header has length %jd "
612                 "that does not match record footer (%jd)";
613       }
614     }
615   }
616   if (error) {
617     handler.SignalError(error, static_cast<std::intmax_t>(currentRecordNumber),
618         static_cast<std::intmax_t>(frameOffsetInFile_),
619         static_cast<std::intmax_t>(header), static_cast<std::intmax_t>(footer));
620     // TODO: error recovery
621   }
622   positionInRecord = sizeof header;
623 }
624 
625 void ExternalFileUnit::BeginSequentialVariableFormattedInputRecord(
626     IoErrorHandler &handler) {
627   if (this == defaultInput && defaultOutput) {
628     defaultOutput->FlushOutput(handler);
629   }
630   std::size_t length{0};
631   do {
632     std::size_t need{length + 1};
633     length =
634         ReadFrame(frameOffsetInFile_, recordOffsetInFrame_ + need, handler) -
635         recordOffsetInFrame_;
636     if (length < need) {
637       if (length > 0) {
638         // final record w/o \n
639         recordLength = length;
640       } else {
641         handler.SignalEnd();
642       }
643       break;
644     }
645   } while (!SetSequentialVariableFormattedRecordLength());
646 }
647 
648 void ExternalFileUnit::BackspaceFixedRecord(IoErrorHandler &handler) {
649   RUNTIME_CHECK(handler, recordLength.has_value());
650   if (frameOffsetInFile_ < *recordLength) {
651     handler.SignalError(IostatBackspaceAtFirstRecord);
652   } else {
653     frameOffsetInFile_ -= *recordLength;
654   }
655 }
656 
657 void ExternalFileUnit::BackspaceVariableUnformattedRecord(
658     IoErrorHandler &handler) {
659   std::int32_t header{0}, footer{0};
660   auto headerBytes{static_cast<std::int64_t>(sizeof header)};
661   frameOffsetInFile_ += recordOffsetInFrame_;
662   recordOffsetInFrame_ = 0;
663   if (frameOffsetInFile_ <= headerBytes) {
664     handler.SignalError(IostatBackspaceAtFirstRecord);
665     return;
666   }
667   // Error conditions here cause crashes, not file format errors, because the
668   // validity of the file structure before the current record will have been
669   // checked informatively in NextSequentialVariableUnformattedInputRecord().
670   std::size_t got{
671       ReadFrame(frameOffsetInFile_ - headerBytes, headerBytes, handler)};
672   RUNTIME_CHECK(handler, got >= sizeof footer);
673   std::memcpy(&footer, Frame(), sizeof footer);
674   recordLength = footer;
675   RUNTIME_CHECK(handler, frameOffsetInFile_ >= *recordLength + 2 * headerBytes);
676   frameOffsetInFile_ -= *recordLength + 2 * headerBytes;
677   if (frameOffsetInFile_ >= headerBytes) {
678     frameOffsetInFile_ -= headerBytes;
679     recordOffsetInFrame_ = headerBytes;
680   }
681   auto need{static_cast<std::size_t>(
682       recordOffsetInFrame_ + sizeof header + *recordLength)};
683   got = ReadFrame(frameOffsetInFile_, need, handler);
684   RUNTIME_CHECK(handler, got >= need);
685   std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header);
686   RUNTIME_CHECK(handler, header == *recordLength);
687 }
688 
689 // There's no portable memrchr(), unfortunately, and strrchr() would
690 // fail on a record with a NUL, so we have to do it the hard way.
691 static const char *FindLastNewline(const char *str, std::size_t length) {
692   for (const char *p{str + length}; p-- > str;) {
693     if (*p == '\n') {
694       return p;
695     }
696   }
697   return nullptr;
698 }
699 
700 void ExternalFileUnit::BackspaceVariableFormattedRecord(
701     IoErrorHandler &handler) {
702   // File offset of previous record's newline
703   auto prevNL{
704       frameOffsetInFile_ + static_cast<std::int64_t>(recordOffsetInFrame_) - 1};
705   if (prevNL < 0) {
706     handler.SignalError(IostatBackspaceAtFirstRecord);
707     return;
708   }
709   while (true) {
710     if (frameOffsetInFile_ < prevNL) {
711       if (const char *p{
712               FindLastNewline(Frame(), prevNL - 1 - frameOffsetInFile_)}) {
713         recordOffsetInFrame_ = p - Frame() + 1;
714         recordLength = prevNL - (frameOffsetInFile_ + recordOffsetInFrame_);
715         break;
716       }
717     }
718     if (frameOffsetInFile_ == 0) {
719       recordOffsetInFrame_ = 0;
720       recordLength = prevNL;
721       break;
722     }
723     frameOffsetInFile_ -= std::min<std::int64_t>(frameOffsetInFile_, 1024);
724     auto need{static_cast<std::size_t>(prevNL + 1 - frameOffsetInFile_)};
725     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
726     RUNTIME_CHECK(handler, got >= need);
727   }
728   RUNTIME_CHECK(handler, Frame()[recordOffsetInFrame_ + *recordLength] == '\n');
729   if (*recordLength > 0 &&
730       Frame()[recordOffsetInFrame_ + *recordLength - 1] == '\r') {
731     --*recordLength;
732   }
733 }
734 
735 void ExternalFileUnit::DoImpliedEndfile(IoErrorHandler &handler) {
736   if (impliedEndfile_) {
737     impliedEndfile_ = false;
738     if (access == Access::Sequential && mayPosition()) {
739       DoEndfile(handler);
740     }
741   }
742 }
743 
744 void ExternalFileUnit::DoEndfile(IoErrorHandler &handler) {
745   endfileRecordNumber = currentRecordNumber;
746   Truncate(frameOffsetInFile_ + recordOffsetInFrame_, handler);
747   BeginRecord();
748   impliedEndfile_ = false;
749 }
750 
751 void ExternalFileUnit::CommitWrites() {
752   frameOffsetInFile_ +=
753       recordOffsetInFrame_ + recordLength.value_or(furthestPositionInRecord);
754   recordOffsetInFrame_ = 0;
755   BeginRecord();
756 }
757 
758 ChildIo &ExternalFileUnit::PushChildIo(IoStatementState &parent) {
759   OwningPtr<ChildIo> current{std::move(child_)};
760   Terminator &terminator{parent.GetIoErrorHandler()};
761   OwningPtr<ChildIo> next{New<ChildIo>{terminator}(parent, std::move(current))};
762   child_.reset(next.release());
763   return *child_;
764 }
765 
766 void ExternalFileUnit::PopChildIo(ChildIo &child) {
767   if (child_.get() != &child) {
768     child.parent().GetIoErrorHandler().Crash(
769         "ChildIo being popped is not top of stack");
770   }
771   child_.reset(child.AcquirePrevious().release()); // deletes top child
772 }
773 
774 void ChildIo::EndIoStatement() {
775   io_.reset();
776   u_.emplace<std::monostate>();
777 }
778 
779 bool ChildIo::CheckFormattingAndDirection(Terminator &terminator,
780     const char *what, bool unformatted, Direction direction) {
781   bool parentIsInput{!parent_.get_if<IoDirectionState<Direction::Output>>()};
782   bool parentIsFormatted{parentIsInput
783           ? parent_.get_if<FormattedIoStatementState<Direction::Input>>() !=
784               nullptr
785           : parent_.get_if<FormattedIoStatementState<Direction::Output>>() !=
786               nullptr};
787   bool parentIsUnformatted{!parentIsFormatted};
788   if (unformatted != parentIsUnformatted) {
789     terminator.Crash("Child %s attempted on %s parent I/O unit", what,
790         parentIsUnformatted ? "unformatted" : "formatted");
791     return false;
792   } else if (parentIsInput != (direction == Direction::Input)) {
793     terminator.Crash("Child %s attempted on %s parent I/O unit", what,
794         parentIsInput ? "input" : "output");
795     return false;
796   } else {
797     return true;
798   }
799 }
800 
801 } // namespace Fortran::runtime::io
802