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