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("%d is not an open I/O unit number", 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 (IsConnected()) {
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 (!openRecl) {
133       handler.SignalError(IostatOpenBadRecl,
134           "OPEN(UNIT=%d,ACCESS='DIRECT'): record length is not known",
135           unitNumber());
136     } else if (*openRecl <= 0) {
137       handler.SignalError(IostatOpenBadRecl,
138           "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is invalid",
139           unitNumber(), static_cast<std::intmax_t>(*openRecl));
140     } else if (totalBytes && (*totalBytes % *openRecl != 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>(*openRecl),
145           static_cast<std::intmax_t>(*totalBytes));
146     }
147     recordLength = openRecl;
148   }
149   endfileRecordNumber.reset();
150   currentRecordNumber = 1;
151   if (totalBytes && access == Access::Direct && openRecl.value_or(0) > 0) {
152     endfileRecordNumber = 1 + (*totalBytes / *openRecl);
153   }
154   if (position == Position::Append && access != Access::Stream) {
155     if (!endfileRecordNumber) {
156       // Fake it so that we can backspace relative from the end
157       endfileRecordNumber = std::numeric_limits<std::int64_t>::max() - 2;
158     }
159     currentRecordNumber = *endfileRecordNumber;
160   }
161 }
162 
163 void ExternalFileUnit::OpenAnonymousUnit(std::optional<OpenStatus> status,
164     std::optional<Action> action, Position position, Convert convert,
165     IoErrorHandler &handler) {
166   // I/O to an unconnected unit reads/creates a local file, e.g. fort.7
167   std::size_t pathMaxLen{32};
168   auto path{SizedNew<char>{handler}(pathMaxLen)};
169   std::snprintf(path.get(), pathMaxLen, "fort.%d", unitNumber_);
170   OpenUnit(status, action, position, std::move(path), std::strlen(path.get()),
171       convert, handler);
172 }
173 
174 void ExternalFileUnit::CloseUnit(CloseStatus status, IoErrorHandler &handler) {
175   DoImpliedEndfile(handler);
176   FlushOutput(handler);
177   Close(status, handler);
178 }
179 
180 void ExternalFileUnit::DestroyClosed() {
181   GetUnitMap().DestroyClosed(*this); // destroys *this
182 }
183 
184 Iostat ExternalFileUnit::SetDirection(Direction direction) {
185   if (direction == Direction::Input) {
186     if (mayRead()) {
187       direction_ = Direction::Input;
188       return IostatOk;
189     } else {
190       return IostatReadFromWriteOnly;
191     }
192   } else {
193     if (mayWrite()) {
194       direction_ = Direction::Output;
195       return IostatOk;
196     } else {
197       return IostatWriteToReadOnly;
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 
214   bool wasExtant{false};
215   ExternalFileUnit &out{newUnitMap->LookUpOrCreate(6, terminator, wasExtant)};
216   RUNTIME_CHECK(terminator, !wasExtant);
217   out.Predefine(1);
218   handler.SignalError(out.SetDirection(Direction::Output));
219   out.isUnformatted = false;
220   defaultOutput = &out;
221 
222   ExternalFileUnit &in{newUnitMap->LookUpOrCreate(5, terminator, wasExtant)};
223   RUNTIME_CHECK(terminator, !wasExtant);
224   in.Predefine(0);
225   handler.SignalError(in.SetDirection(Direction::Input));
226   in.isUnformatted = false;
227   defaultInput = &in;
228 
229   ExternalFileUnit &error{newUnitMap->LookUpOrCreate(0, terminator, wasExtant)};
230   RUNTIME_CHECK(terminator, !wasExtant);
231   error.Predefine(2);
232   handler.SignalError(error.SetDirection(Direction::Output));
233   error.isUnformatted = false;
234   errorOutput = &error;
235 
236   // TODO: Set UTF-8 mode from the environment
237   unitMap = newUnitMap;
238   return *unitMap;
239 }
240 
241 void ExternalFileUnit::CloseAll(IoErrorHandler &handler) {
242   CriticalSection critical{unitMapLock};
243   if (unitMap) {
244     unitMap->CloseAll(handler);
245     FreeMemoryAndNullify(unitMap);
246   }
247   defaultOutput = nullptr;
248   defaultInput = nullptr;
249   errorOutput = nullptr;
250 }
251 
252 void ExternalFileUnit::FlushAll(IoErrorHandler &handler) {
253   CriticalSection critical{unitMapLock};
254   if (unitMap) {
255     unitMap->FlushAll(handler);
256   }
257 }
258 
259 static void SwapEndianness(
260     char *data, std::size_t bytes, std::size_t elementBytes) {
261   if (elementBytes > 1) {
262     auto half{elementBytes >> 1};
263     for (std::size_t j{0}; j + elementBytes <= bytes; j += elementBytes) {
264       for (std::size_t k{0}; k < half; ++k) {
265         std::swap(data[j + k], data[j + elementBytes - 1 - k]);
266       }
267     }
268   }
269 }
270 
271 bool ExternalFileUnit::Emit(const char *data, std::size_t bytes,
272     std::size_t elementBytes, IoErrorHandler &handler) {
273   auto furthestAfter{std::max(furthestPositionInRecord,
274       positionInRecord + static_cast<std::int64_t>(bytes))};
275   if (openRecl) {
276     // Check for fixed-length record overrun, but allow for
277     // sequential record termination.
278     int extra{0};
279     int header{0};
280     if (access == Access::Sequential) {
281       if (isUnformatted.value_or(false)) {
282         // record header + footer
283         header = static_cast<int>(sizeof(std::uint32_t));
284         extra = 2 * header;
285       } else {
286 #ifdef _WIN32
287         if (!isWindowsTextFile()) {
288           ++extra; // carriage return (CR)
289         }
290 #endif
291         ++extra; // newline (LF)
292       }
293     }
294     if (furthestAfter > extra + *openRecl) {
295       handler.SignalError(IostatRecordWriteOverrun,
296           "Attempt to write %zd bytes to position %jd in a fixed-size record "
297           "of %jd bytes",
298           bytes, static_cast<std::intmax_t>(positionInRecord - header),
299           static_cast<std::intmax_t>(*openRecl));
300       return false;
301     }
302   }
303   if (recordLength) {
304     // It is possible for recordLength to have a value now for a
305     // variable-length output record if the previous operation
306     // was a BACKSPACE or non advancing input statement.
307     recordLength.reset();
308     beganReadingRecord_ = false;
309   }
310   if (IsAfterEndfile()) {
311     handler.SignalError(IostatWriteAfterEndfile);
312     return false;
313   }
314   CheckDirectAccess(handler);
315   WriteFrame(frameOffsetInFile_, recordOffsetInFrame_ + furthestAfter, handler);
316   if (positionInRecord > furthestPositionInRecord) {
317     std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord, ' ',
318         positionInRecord - furthestPositionInRecord);
319   }
320   char *to{Frame() + recordOffsetInFrame_ + positionInRecord};
321   std::memcpy(to, data, bytes);
322   if (swapEndianness_) {
323     SwapEndianness(to, bytes, elementBytes);
324   }
325   positionInRecord += bytes;
326   furthestPositionInRecord = furthestAfter;
327   return true;
328 }
329 
330 bool ExternalFileUnit::Receive(char *data, std::size_t bytes,
331     std::size_t elementBytes, IoErrorHandler &handler) {
332   RUNTIME_CHECK(handler, direction_ == Direction::Input);
333   auto furthestAfter{std::max(furthestPositionInRecord,
334       positionInRecord + static_cast<std::int64_t>(bytes))};
335   if (furthestAfter > recordLength.value_or(furthestAfter)) {
336     handler.SignalError(IostatRecordReadOverrun,
337         "Attempt to read %zd bytes at position %jd in a record of %jd bytes",
338         bytes, static_cast<std::intmax_t>(positionInRecord),
339         static_cast<std::intmax_t>(*recordLength));
340     return false;
341   }
342   auto need{recordOffsetInFrame_ + furthestAfter};
343   auto got{ReadFrame(frameOffsetInFile_, need, handler)};
344   if (got >= need) {
345     std::memcpy(data, Frame() + recordOffsetInFrame_ + positionInRecord, bytes);
346     if (swapEndianness_) {
347       SwapEndianness(data, bytes, elementBytes);
348     }
349     positionInRecord += bytes;
350     furthestPositionInRecord = furthestAfter;
351     return true;
352   } else {
353     handler.SignalEnd();
354     if (access == Access::Sequential) {
355       endfileRecordNumber = currentRecordNumber;
356     }
357     return false;
358   }
359 }
360 
361 std::size_t ExternalFileUnit::GetNextInputBytes(
362     const char *&p, IoErrorHandler &handler) {
363   RUNTIME_CHECK(handler, direction_ == Direction::Input);
364   std::size_t length{1};
365   if (auto recl{EffectiveRecordLength()}) {
366     if (positionInRecord < *recl) {
367       length = *recl - positionInRecord;
368     } else {
369       p = nullptr;
370       return 0;
371     }
372   }
373   p = FrameNextInput(handler, length);
374   return p ? length : 0;
375 }
376 
377 std::optional<char32_t> ExternalFileUnit::GetCurrentChar(
378     IoErrorHandler &handler) {
379   const char *p{nullptr};
380   std::size_t bytes{GetNextInputBytes(p, handler)};
381   if (bytes == 0) {
382     return std::nullopt;
383   } else {
384     // TODO: UTF-8 decoding; may have to get more bytes in a loop
385     return *p;
386   }
387 }
388 
389 const char *ExternalFileUnit::FrameNextInput(
390     IoErrorHandler &handler, std::size_t bytes) {
391   RUNTIME_CHECK(handler, isUnformatted.has_value() && !*isUnformatted);
392   if (static_cast<std::int64_t>(positionInRecord + bytes) <=
393       recordLength.value_or(positionInRecord + bytes)) {
394     auto at{recordOffsetInFrame_ + positionInRecord};
395     auto need{static_cast<std::size_t>(at + bytes)};
396     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
397     SetVariableFormattedRecordLength();
398     if (got >= need) {
399       return Frame() + at;
400     }
401     handler.SignalEnd();
402     if (access == Access::Sequential) {
403       endfileRecordNumber = currentRecordNumber;
404     }
405   }
406   return nullptr;
407 }
408 
409 bool ExternalFileUnit::SetVariableFormattedRecordLength() {
410   if (recordLength || access == Access::Direct) {
411     return true;
412   } else if (FrameLength() > recordOffsetInFrame_) {
413     const char *record{Frame() + recordOffsetInFrame_};
414     std::size_t bytes{FrameLength() - recordOffsetInFrame_};
415     if (const char *nl{
416             reinterpret_cast<const char *>(std::memchr(record, '\n', bytes))}) {
417       recordLength = nl - record;
418       if (*recordLength > 0 && record[*recordLength - 1] == '\r') {
419         --*recordLength;
420       }
421       return true;
422     }
423   }
424   return false;
425 }
426 
427 void ExternalFileUnit::SetLeftTabLimit() {
428   leftTabLimit = furthestPositionInRecord;
429   positionInRecord = furthestPositionInRecord;
430 }
431 
432 bool ExternalFileUnit::BeginReadingRecord(IoErrorHandler &handler) {
433   RUNTIME_CHECK(handler, direction_ == Direction::Input);
434   if (!beganReadingRecord_) {
435     beganReadingRecord_ = true;
436     if (access == Access::Direct) {
437       CheckDirectAccess(handler);
438       auto need{static_cast<std::size_t>(recordOffsetInFrame_ + *openRecl)};
439       auto got{ReadFrame(frameOffsetInFile_, need, handler)};
440       if (got >= need) {
441         recordLength = openRecl;
442       } else {
443         recordLength.reset();
444         handler.SignalEnd();
445       }
446     } else {
447       recordLength.reset();
448       if (IsAtEOF()) {
449         handler.SignalEnd();
450       } else {
451         RUNTIME_CHECK(handler, isUnformatted.has_value());
452         if (*isUnformatted) {
453           if (access == Access::Sequential) {
454             BeginSequentialVariableUnformattedInputRecord(handler);
455           }
456         } else { // formatted sequential or stream
457           BeginVariableFormattedInputRecord(handler);
458         }
459       }
460     }
461   }
462   RUNTIME_CHECK(handler,
463       recordLength.has_value() || !IsRecordFile() || handler.InError());
464   return !handler.InError();
465 }
466 
467 void ExternalFileUnit::FinishReadingRecord(IoErrorHandler &handler) {
468   RUNTIME_CHECK(handler, direction_ == Direction::Input && beganReadingRecord_);
469   beganReadingRecord_ = false;
470   if (handler.InError() && handler.GetIoStat() != IostatEor) {
471     // Avoid bogus crashes in END/ERR circumstances; but
472     // still increment the current record number so that
473     // an attempted read of an endfile record, followed by
474     // a BACKSPACE, will still be at EOF.
475     ++currentRecordNumber;
476   } else if (IsRecordFile()) {
477     RUNTIME_CHECK(handler, recordLength.has_value());
478     recordOffsetInFrame_ += *recordLength;
479     if (access != Access::Direct) {
480       RUNTIME_CHECK(handler, isUnformatted.has_value());
481       recordLength.reset();
482       if (isUnformatted.value_or(false)) {
483         // Retain footer in frame for more efficient BACKSPACE
484         frameOffsetInFile_ += recordOffsetInFrame_;
485         recordOffsetInFrame_ = sizeof(std::uint32_t);
486       } else { // formatted
487         if (FrameLength() > recordOffsetInFrame_ &&
488             Frame()[recordOffsetInFrame_] == '\r') {
489           ++recordOffsetInFrame_;
490         }
491         if (FrameLength() > recordOffsetInFrame_ &&
492             Frame()[recordOffsetInFrame_] == '\n') {
493           ++recordOffsetInFrame_;
494         }
495         if (!pinnedFrame || mayPosition()) {
496           frameOffsetInFile_ += recordOffsetInFrame_;
497           recordOffsetInFrame_ = 0;
498         }
499       }
500     }
501     ++currentRecordNumber;
502   } else { // unformatted stream
503     furthestPositionInRecord =
504         std::max(furthestPositionInRecord, positionInRecord);
505     frameOffsetInFile_ += recordOffsetInFrame_ + furthestPositionInRecord;
506   }
507   BeginRecord();
508 }
509 
510 bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) {
511   if (direction_ == Direction::Input) {
512     FinishReadingRecord(handler);
513     return BeginReadingRecord(handler);
514   } else { // Direction::Output
515     bool ok{true};
516     RUNTIME_CHECK(handler, isUnformatted.has_value());
517     positionInRecord = furthestPositionInRecord;
518     if (access == Access::Direct) {
519       if (furthestPositionInRecord <
520           openRecl.value_or(furthestPositionInRecord)) {
521         // Pad remainder of fixed length record
522         WriteFrame(
523             frameOffsetInFile_, recordOffsetInFrame_ + *openRecl, handler);
524         std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord,
525             isUnformatted.value_or(false) ? 0 : ' ',
526             *openRecl - furthestPositionInRecord);
527         furthestPositionInRecord = *openRecl;
528       }
529     } else if (*isUnformatted) {
530       if (access == Access::Sequential) {
531         // Append the length of a sequential unformatted variable-length record
532         // as its footer, then overwrite the reserved first four bytes of the
533         // record with its length as its header.  These four bytes were skipped
534         // over in BeginUnformattedIO<Output>().
535         // TODO: Break very large records up into subrecords with negative
536         // headers &/or footers
537         std::uint32_t length;
538         length = furthestPositionInRecord - sizeof length;
539         ok = ok &&
540             Emit(reinterpret_cast<const char *>(&length), sizeof length,
541                 sizeof length, handler);
542         positionInRecord = 0;
543         ok = ok &&
544             Emit(reinterpret_cast<const char *>(&length), sizeof length,
545                 sizeof length, handler);
546       } else {
547         // Unformatted stream: nothing to do
548       }
549     } else {
550       // Terminate formatted variable length record
551       const char *lineEnding{"\n"};
552       std::size_t lineEndingBytes{1};
553 #ifdef _WIN32
554       if (!isWindowsTextFile()) {
555         lineEnding = "\r\n";
556         lineEndingBytes = 2;
557       }
558 #endif
559       ok = ok && Emit(lineEnding, lineEndingBytes, 1, handler);
560     }
561     if (IsAfterEndfile()) {
562       return false;
563     }
564     CommitWrites();
565     ++currentRecordNumber;
566     if (access != Access::Direct) {
567       impliedEndfile_ = IsRecordFile();
568       if (IsAtEOF()) {
569         endfileRecordNumber.reset();
570       }
571     }
572     return ok;
573   }
574 }
575 
576 void ExternalFileUnit::BackspaceRecord(IoErrorHandler &handler) {
577   if (access == Access::Direct || !IsRecordFile()) {
578     handler.SignalError(IostatBackspaceNonSequential,
579         "BACKSPACE(UNIT=%d) on direct-access file or unformatted stream",
580         unitNumber());
581   } else {
582     if (IsAfterEndfile()) {
583       // BACKSPACE after explicit ENDFILE
584       currentRecordNumber = *endfileRecordNumber;
585     } else {
586       DoImpliedEndfile(handler);
587       if (frameOffsetInFile_ + recordOffsetInFrame_ > 0) {
588         --currentRecordNumber;
589         if (openRecl && access == Access::Direct) {
590           BackspaceFixedRecord(handler);
591         } else {
592           RUNTIME_CHECK(handler, isUnformatted.has_value());
593           if (isUnformatted.value_or(false)) {
594             BackspaceVariableUnformattedRecord(handler);
595           } else {
596             BackspaceVariableFormattedRecord(handler);
597           }
598         }
599       }
600     }
601     BeginRecord();
602   }
603 }
604 
605 void ExternalFileUnit::FlushOutput(IoErrorHandler &handler) {
606   if (!mayPosition()) {
607     auto frameAt{FrameAt()};
608     if (frameOffsetInFile_ >= frameAt &&
609         frameOffsetInFile_ <
610             static_cast<std::int64_t>(frameAt + FrameLength())) {
611       // A Flush() that's about to happen to a non-positionable file
612       // needs to advance frameOffsetInFile_ to prevent attempts at
613       // impossible seeks
614       CommitWrites();
615     }
616   }
617   Flush(handler);
618 }
619 
620 void ExternalFileUnit::FlushIfTerminal(IoErrorHandler &handler) {
621   if (isTerminal()) {
622     FlushOutput(handler);
623   }
624 }
625 
626 void ExternalFileUnit::Endfile(IoErrorHandler &handler) {
627   if (access == Access::Direct) {
628     handler.SignalError(IostatEndfileDirect,
629         "ENDFILE(UNIT=%d) on direct-access file", unitNumber());
630   } else if (!mayWrite()) {
631     handler.SignalError(IostatEndfileUnwritable,
632         "ENDFILE(UNIT=%d) on read-only file", unitNumber());
633   } else if (IsAfterEndfile()) {
634     // ENDFILE after ENDFILE
635   } else {
636     DoEndfile(handler);
637     if (access == Access::Sequential) {
638       // Explicit ENDFILE leaves position *after* the endfile record
639       RUNTIME_CHECK(handler, endfileRecordNumber.has_value());
640       currentRecordNumber = *endfileRecordNumber + 1;
641     }
642   }
643 }
644 
645 void ExternalFileUnit::Rewind(IoErrorHandler &handler) {
646   if (access == Access::Direct) {
647     handler.SignalError(IostatRewindNonSequential,
648         "REWIND(UNIT=%d) on non-sequential file", unitNumber());
649   } else {
650     SetPosition(0, handler);
651     currentRecordNumber = 1;
652   }
653 }
654 
655 void ExternalFileUnit::SetPosition(std::int64_t pos, IoErrorHandler &handler) {
656   DoImpliedEndfile(handler);
657   frameOffsetInFile_ = pos;
658   recordOffsetInFrame_ = 0;
659   if (access == Access::Direct) {
660     directAccessRecWasSet_ = true;
661   }
662   BeginRecord();
663 }
664 
665 void ExternalFileUnit::EndIoStatement() {
666   io_.reset();
667   u_.emplace<std::monostate>();
668   lock_.Drop();
669 }
670 
671 void ExternalFileUnit::BeginSequentialVariableUnformattedInputRecord(
672     IoErrorHandler &handler) {
673   std::int32_t header{0}, footer{0};
674   std::size_t need{recordOffsetInFrame_ + sizeof header};
675   std::size_t got{ReadFrame(frameOffsetInFile_, need, handler)};
676   // Try to emit informative errors to help debug corrupted files.
677   const char *error{nullptr};
678   if (got < need) {
679     if (got == recordOffsetInFrame_) {
680       handler.SignalEnd();
681     } else {
682       error = "Unformatted variable-length sequential file input failed at "
683               "record #%jd (file offset %jd): truncated record header";
684     }
685   } else {
686     std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header);
687     recordLength = sizeof header + header; // does not include footer
688     need = recordOffsetInFrame_ + *recordLength + sizeof footer;
689     got = ReadFrame(frameOffsetInFile_, need, handler);
690     if (got < need) {
691       error = "Unformatted variable-length sequential file input failed at "
692               "record #%jd (file offset %jd): hit EOF reading record with "
693               "length %jd bytes";
694     } else {
695       std::memcpy(&footer, Frame() + recordOffsetInFrame_ + *recordLength,
696           sizeof footer);
697       if (footer != header) {
698         error = "Unformatted variable-length sequential file input failed at "
699                 "record #%jd (file offset %jd): record header has length %jd "
700                 "that does not match record footer (%jd)";
701       }
702     }
703   }
704   if (error) {
705     handler.SignalError(error, static_cast<std::intmax_t>(currentRecordNumber),
706         static_cast<std::intmax_t>(frameOffsetInFile_),
707         static_cast<std::intmax_t>(header), static_cast<std::intmax_t>(footer));
708     // TODO: error recovery
709   }
710   positionInRecord = sizeof header;
711 }
712 
713 void ExternalFileUnit::BeginVariableFormattedInputRecord(
714     IoErrorHandler &handler) {
715   if (this == defaultInput) {
716     if (defaultOutput) {
717       defaultOutput->FlushOutput(handler);
718     }
719     if (errorOutput) {
720       errorOutput->FlushOutput(handler);
721     }
722   }
723   std::size_t length{0};
724   do {
725     std::size_t need{length + 1};
726     length =
727         ReadFrame(frameOffsetInFile_, recordOffsetInFrame_ + need, handler) -
728         recordOffsetInFrame_;
729     if (length < need) {
730       if (length > 0) {
731         // final record w/o \n
732         recordLength = length;
733       } else {
734         handler.SignalEnd();
735       }
736       break;
737     }
738   } while (!SetVariableFormattedRecordLength());
739 }
740 
741 void ExternalFileUnit::BackspaceFixedRecord(IoErrorHandler &handler) {
742   RUNTIME_CHECK(handler, openRecl.has_value());
743   if (frameOffsetInFile_ < *openRecl) {
744     handler.SignalError(IostatBackspaceAtFirstRecord);
745   } else {
746     frameOffsetInFile_ -= *openRecl;
747   }
748 }
749 
750 void ExternalFileUnit::BackspaceVariableUnformattedRecord(
751     IoErrorHandler &handler) {
752   std::int32_t header{0}, footer{0};
753   auto headerBytes{static_cast<std::int64_t>(sizeof header)};
754   frameOffsetInFile_ += recordOffsetInFrame_;
755   recordOffsetInFrame_ = 0;
756   if (frameOffsetInFile_ <= headerBytes) {
757     handler.SignalError(IostatBackspaceAtFirstRecord);
758     return;
759   }
760   // Error conditions here cause crashes, not file format errors, because the
761   // validity of the file structure before the current record will have been
762   // checked informatively in NextSequentialVariableUnformattedInputRecord().
763   std::size_t got{
764       ReadFrame(frameOffsetInFile_ - headerBytes, headerBytes, handler)};
765   RUNTIME_CHECK(handler, got >= sizeof footer);
766   std::memcpy(&footer, Frame(), sizeof footer);
767   recordLength = footer;
768   RUNTIME_CHECK(handler, frameOffsetInFile_ >= *recordLength + 2 * headerBytes);
769   frameOffsetInFile_ -= *recordLength + 2 * headerBytes;
770   if (frameOffsetInFile_ >= headerBytes) {
771     frameOffsetInFile_ -= headerBytes;
772     recordOffsetInFrame_ = headerBytes;
773   }
774   auto need{static_cast<std::size_t>(
775       recordOffsetInFrame_ + sizeof header + *recordLength)};
776   got = ReadFrame(frameOffsetInFile_, need, handler);
777   RUNTIME_CHECK(handler, got >= need);
778   std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header);
779   RUNTIME_CHECK(handler, header == *recordLength);
780 }
781 
782 // There's no portable memrchr(), unfortunately, and strrchr() would
783 // fail on a record with a NUL, so we have to do it the hard way.
784 static const char *FindLastNewline(const char *str, std::size_t length) {
785   for (const char *p{str + length}; p-- > str;) {
786     if (*p == '\n') {
787       return p;
788     }
789   }
790   return nullptr;
791 }
792 
793 void ExternalFileUnit::BackspaceVariableFormattedRecord(
794     IoErrorHandler &handler) {
795   // File offset of previous record's newline
796   auto prevNL{
797       frameOffsetInFile_ + static_cast<std::int64_t>(recordOffsetInFrame_) - 1};
798   if (prevNL < 0) {
799     handler.SignalError(IostatBackspaceAtFirstRecord);
800     return;
801   }
802   while (true) {
803     if (frameOffsetInFile_ < prevNL) {
804       if (const char *p{
805               FindLastNewline(Frame(), prevNL - 1 - frameOffsetInFile_)}) {
806         recordOffsetInFrame_ = p - Frame() + 1;
807         recordLength = prevNL - (frameOffsetInFile_ + recordOffsetInFrame_);
808         break;
809       }
810     }
811     if (frameOffsetInFile_ == 0) {
812       recordOffsetInFrame_ = 0;
813       recordLength = prevNL;
814       break;
815     }
816     frameOffsetInFile_ -= std::min<std::int64_t>(frameOffsetInFile_, 1024);
817     auto need{static_cast<std::size_t>(prevNL + 1 - frameOffsetInFile_)};
818     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
819     RUNTIME_CHECK(handler, got >= need);
820   }
821   RUNTIME_CHECK(handler, Frame()[recordOffsetInFrame_ + *recordLength] == '\n');
822   if (*recordLength > 0 &&
823       Frame()[recordOffsetInFrame_ + *recordLength - 1] == '\r') {
824     --*recordLength;
825   }
826 }
827 
828 void ExternalFileUnit::DoImpliedEndfile(IoErrorHandler &handler) {
829   if (impliedEndfile_) {
830     impliedEndfile_ = false;
831     if (access != Access::Direct && IsRecordFile() && mayPosition()) {
832       DoEndfile(handler);
833     }
834   }
835 }
836 
837 void ExternalFileUnit::DoEndfile(IoErrorHandler &handler) {
838   if (access == Access::Sequential) {
839     endfileRecordNumber = currentRecordNumber;
840   }
841   FlushOutput(handler);
842   Truncate(frameOffsetInFile_ + recordOffsetInFrame_, handler);
843   BeginRecord();
844   impliedEndfile_ = false;
845 }
846 
847 void ExternalFileUnit::CommitWrites() {
848   frameOffsetInFile_ +=
849       recordOffsetInFrame_ + recordLength.value_or(furthestPositionInRecord);
850   recordOffsetInFrame_ = 0;
851   BeginRecord();
852 }
853 
854 bool ExternalFileUnit::CheckDirectAccess(IoErrorHandler &handler) {
855   if (access == Access::Direct) {
856     RUNTIME_CHECK(handler, openRecl);
857     if (!directAccessRecWasSet_) {
858       handler.SignalError(
859           "No REC= was specified for a data transfer with ACCESS='DIRECT'");
860       return false;
861     }
862   }
863   return true;
864 }
865 
866 ChildIo &ExternalFileUnit::PushChildIo(IoStatementState &parent) {
867   OwningPtr<ChildIo> current{std::move(child_)};
868   Terminator &terminator{parent.GetIoErrorHandler()};
869   OwningPtr<ChildIo> next{New<ChildIo>{terminator}(parent, std::move(current))};
870   child_.reset(next.release());
871   return *child_;
872 }
873 
874 void ExternalFileUnit::PopChildIo(ChildIo &child) {
875   if (child_.get() != &child) {
876     child.parent().GetIoErrorHandler().Crash(
877         "ChildIo being popped is not top of stack");
878   }
879   child_.reset(child.AcquirePrevious().release()); // deletes top child
880 }
881 
882 void ChildIo::EndIoStatement() {
883   io_.reset();
884   u_.emplace<std::monostate>();
885 }
886 
887 Iostat ChildIo::CheckFormattingAndDirection(
888     bool unformatted, Direction direction) {
889   bool parentIsInput{!parent_.get_if<IoDirectionState<Direction::Output>>()};
890   bool parentIsFormatted{parentIsInput
891           ? parent_.get_if<FormattedIoStatementState<Direction::Input>>() !=
892               nullptr
893           : parent_.get_if<FormattedIoStatementState<Direction::Output>>() !=
894               nullptr};
895   bool parentIsUnformatted{!parentIsFormatted};
896   if (unformatted != parentIsUnformatted) {
897     return unformatted ? IostatUnformattedChildOnFormattedParent
898                        : IostatFormattedChildOnUnformattedParent;
899   } else if (parentIsInput != (direction == Direction::Input)) {
900     return parentIsInput ? IostatChildOutputToInputParent
901                          : IostatChildInputFromOutputParent;
902   } else {
903     return IostatOk;
904   }
905 }
906 
907 } // namespace Fortran::runtime::io
908