1 //===-- runtime/unit.cpp ----------------------------------------*- C++ -*-===//
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 <utility>
16 
17 namespace Fortran::runtime::io {
18 
19 // The per-unit data structures are created on demand so that Fortran I/O
20 // should work without a Fortran main program.
21 static Lock unitMapLock;
22 static UnitMap *unitMap{nullptr};
23 static ExternalFileUnit *defaultInput{nullptr};
24 static ExternalFileUnit *defaultOutput{nullptr};
25 
26 void FlushOutputOnCrash(const Terminator &terminator) {
27   if (!defaultOutput) {
28     return;
29   }
30   CriticalSection critical{unitMapLock};
31   if (defaultOutput) {
32     IoErrorHandler handler{terminator};
33     handler.HasIoStat(); // prevent nested crash if flush has error
34     defaultOutput->Flush(handler);
35   }
36 }
37 
38 ExternalFileUnit *ExternalFileUnit::LookUp(int unit) {
39   return GetUnitMap().LookUp(unit);
40 }
41 
42 ExternalFileUnit &ExternalFileUnit::LookUpOrCrash(
43     int unit, const Terminator &terminator) {
44   ExternalFileUnit *file{LookUp(unit)};
45   if (!file) {
46     terminator.Crash("Not an open I/O unit number: %d", unit);
47   }
48   return *file;
49 }
50 
51 ExternalFileUnit &ExternalFileUnit::LookUpOrCreate(
52     int unit, const Terminator &terminator, bool &wasExtant) {
53   return GetUnitMap().LookUpOrCreate(unit, terminator, wasExtant);
54 }
55 
56 ExternalFileUnit &ExternalFileUnit::LookUpOrCreateAnonymous(
57     int unit, Direction dir, bool isUnformatted, const Terminator &terminator) {
58   bool exists{false};
59   ExternalFileUnit &result{
60       GetUnitMap().LookUpOrCreate(unit, terminator, exists)};
61   if (!exists) {
62     IoErrorHandler handler{terminator};
63     result.OpenAnonymousUnit(
64         dir == Direction::Input ? OpenStatus::Unknown : OpenStatus::Replace,
65         Action::ReadWrite, Position::Rewind, Convert::Native, handler);
66     result.isUnformatted = isUnformatted;
67   }
68   return result;
69 }
70 
71 ExternalFileUnit *ExternalFileUnit::LookUp(const char *path) {
72   return GetUnitMap().LookUp(path);
73 }
74 
75 ExternalFileUnit &ExternalFileUnit::CreateNew(
76     int unit, const Terminator &terminator) {
77   bool wasExtant{false};
78   ExternalFileUnit &result{
79       GetUnitMap().LookUpOrCreate(unit, terminator, wasExtant)};
80   RUNTIME_CHECK(terminator, !wasExtant);
81   return result;
82 }
83 
84 ExternalFileUnit *ExternalFileUnit::LookUpForClose(int unit) {
85   return GetUnitMap().LookUpForClose(unit);
86 }
87 
88 int ExternalFileUnit::NewUnit(const Terminator &terminator) {
89   return GetUnitMap().NewUnit(terminator).unitNumber();
90 }
91 
92 void ExternalFileUnit::OpenUnit(OpenStatus status, std::optional<Action> action,
93     Position position, OwningPtr<char> &&newPath, std::size_t newPathLength,
94     Convert convert, IoErrorHandler &handler) {
95   if (executionEnvironment.conversion != Convert::Unknown) {
96     convert = executionEnvironment.conversion;
97   }
98   swapEndianness_ = convert == Convert::Swap ||
99       (convert == Convert::LittleEndian && !isHostLittleEndian) ||
100       (convert == Convert::BigEndian && isHostLittleEndian);
101   if (IsOpen()) {
102     if (status == OpenStatus::Old &&
103         (!newPath.get() ||
104             (path() && pathLength() == newPathLength &&
105                 std::memcmp(path(), newPath.get(), newPathLength) == 0))) {
106       // OPEN of existing unit, STATUS='OLD', not new FILE=
107       newPath.reset();
108       return;
109     }
110     // Otherwise, OPEN on open unit with new FILE= implies CLOSE
111     DoImpliedEndfile(handler);
112     Flush(handler);
113     Close(CloseStatus::Keep, handler);
114   }
115   set_path(std::move(newPath), newPathLength);
116   Open(status, action, position, handler);
117   auto totalBytes{knownSize()};
118   if (access == Access::Direct) {
119     if (!isFixedRecordLength || !recordLength) {
120       handler.SignalError(IostatOpenBadRecl,
121           "OPEN(UNIT=%d,ACCESS='DIRECT'): record length is not known",
122           unitNumber());
123     } else if (*recordLength <= 0) {
124       handler.SignalError(IostatOpenBadRecl,
125           "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is invalid",
126           unitNumber(), static_cast<std::intmax_t>(*recordLength));
127     } else if (totalBytes && (*totalBytes % *recordLength != 0)) {
128       handler.SignalError(IostatOpenBadAppend,
129           "OPEN(UNIT=%d,ACCESS='DIRECT',RECL=%jd): record length is not an "
130           "even divisor of the file size %jd",
131           unitNumber(), static_cast<std::intmax_t>(*recordLength),
132           static_cast<std::intmax_t>(*totalBytes));
133     }
134   }
135   if (position == Position::Append) {
136     if (totalBytes && recordLength && *recordLength) {
137       endfileRecordNumber = 1 + (*totalBytes / *recordLength);
138     } else {
139       // Fake it so that we can backspace relative from the end
140       endfileRecordNumber = std::numeric_limits<std::int64_t>::max() - 1;
141     }
142     currentRecordNumber = *endfileRecordNumber;
143   } else {
144     currentRecordNumber = 1;
145   }
146 }
147 
148 void ExternalFileUnit::OpenAnonymousUnit(OpenStatus status,
149     std::optional<Action> action, Position position, Convert convert,
150     IoErrorHandler &handler) {
151   // I/O to an unconnected unit reads/creates a local file, e.g. fort.7
152   std::size_t pathMaxLen{32};
153   auto path{SizedNew<char>{handler}(pathMaxLen)};
154   std::snprintf(path.get(), pathMaxLen, "fort.%d", unitNumber_);
155   OpenUnit(status, action, position, std::move(path), std::strlen(path.get()),
156       convert, handler);
157 }
158 
159 void ExternalFileUnit::CloseUnit(CloseStatus status, IoErrorHandler &handler) {
160   DoImpliedEndfile(handler);
161   Flush(handler);
162   Close(status, handler);
163 }
164 
165 void ExternalFileUnit::DestroyClosed() {
166   GetUnitMap().DestroyClosed(*this); // destroys *this
167 }
168 
169 bool ExternalFileUnit::SetDirection(
170     Direction direction, IoErrorHandler &handler) {
171   if (direction == Direction::Input) {
172     if (mayRead()) {
173       direction_ = Direction::Input;
174       return true;
175     } else {
176       handler.SignalError(IostatReadFromWriteOnly,
177           "READ(UNIT=%d) with ACTION='WRITE'", unitNumber());
178       return false;
179     }
180   } else {
181     if (mayWrite()) {
182       direction_ = Direction::Output;
183       return true;
184     } else {
185       handler.SignalError(IostatWriteToReadOnly,
186           "WRITE(UNIT=%d) with ACTION='READ'", unitNumber());
187       return false;
188     }
189   }
190 }
191 
192 UnitMap &ExternalFileUnit::GetUnitMap() {
193   if (unitMap) {
194     return *unitMap;
195   }
196   CriticalSection critical{unitMapLock};
197   if (unitMap) {
198     return *unitMap;
199   }
200   Terminator terminator{__FILE__, __LINE__};
201   IoErrorHandler handler{terminator};
202   unitMap = New<UnitMap>{terminator}().release();
203   ExternalFileUnit &out{ExternalFileUnit::CreateNew(6, terminator)};
204   out.Predefine(1);
205   out.SetDirection(Direction::Output, handler);
206   defaultOutput = &out;
207   ExternalFileUnit &in{ExternalFileUnit::CreateNew(5, terminator)};
208   in.Predefine(0);
209   in.SetDirection(Direction::Input, handler);
210   defaultInput = &in;
211   // TODO: Set UTF-8 mode from the environment
212   return *unitMap;
213 }
214 
215 void ExternalFileUnit::CloseAll(IoErrorHandler &handler) {
216   CriticalSection critical{unitMapLock};
217   if (unitMap) {
218     unitMap->CloseAll(handler);
219     FreeMemoryAndNullify(unitMap);
220   }
221   defaultOutput = nullptr;
222 }
223 
224 void ExternalFileUnit::FlushAll(IoErrorHandler &handler) {
225   CriticalSection critical{unitMapLock};
226   if (unitMap) {
227     unitMap->FlushAll(handler);
228   }
229 }
230 
231 static void SwapEndianness(
232     char *data, std::size_t bytes, std::size_t elementBytes) {
233   if (elementBytes > 1) {
234     auto half{elementBytes >> 1};
235     for (std::size_t j{0}; j + elementBytes <= bytes; j += elementBytes) {
236       for (std::size_t k{0}; k < half; ++k) {
237         std::swap(data[j + k], data[j + elementBytes - 1 - k]);
238       }
239     }
240   }
241 }
242 
243 bool ExternalFileUnit::Emit(const char *data, std::size_t bytes,
244     std::size_t elementBytes, IoErrorHandler &handler) {
245   auto furthestAfter{std::max(furthestPositionInRecord,
246       positionInRecord + static_cast<std::int64_t>(bytes))};
247   if (furthestAfter > recordLength.value_or(furthestAfter)) {
248     handler.SignalError(IostatRecordWriteOverrun,
249         "Attempt to write %zd bytes to position %jd in a fixed-size record of "
250         "%jd bytes",
251         bytes, static_cast<std::intmax_t>(positionInRecord),
252         static_cast<std::intmax_t>(*recordLength));
253     return false;
254   }
255   WriteFrame(frameOffsetInFile_, recordOffsetInFrame_ + furthestAfter, handler);
256   if (positionInRecord > furthestPositionInRecord) {
257     std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord, ' ',
258         positionInRecord - furthestPositionInRecord);
259   }
260   char *to{Frame() + recordOffsetInFrame_ + positionInRecord};
261   std::memcpy(to, data, bytes);
262   if (swapEndianness_) {
263     SwapEndianness(to, bytes, elementBytes);
264   }
265   positionInRecord += bytes;
266   furthestPositionInRecord = furthestAfter;
267   return true;
268 }
269 
270 bool ExternalFileUnit::Receive(char *data, std::size_t bytes,
271     std::size_t elementBytes, IoErrorHandler &handler) {
272   RUNTIME_CHECK(handler, direction_ == Direction::Input);
273   auto furthestAfter{std::max(furthestPositionInRecord,
274       positionInRecord + static_cast<std::int64_t>(bytes))};
275   if (furthestAfter > recordLength.value_or(furthestAfter)) {
276     handler.SignalError(IostatRecordReadOverrun,
277         "Attempt to read %zd bytes at position %jd in a record of %jd bytes",
278         bytes, static_cast<std::intmax_t>(positionInRecord),
279         static_cast<std::intmax_t>(*recordLength));
280     return false;
281   }
282   auto need{recordOffsetInFrame_ + furthestAfter};
283   auto got{ReadFrame(frameOffsetInFile_, need, handler)};
284   if (got >= need) {
285     std::memcpy(data, Frame() + recordOffsetInFrame_ + positionInRecord, bytes);
286     if (swapEndianness_) {
287       SwapEndianness(data, bytes, elementBytes);
288     }
289     positionInRecord += bytes;
290     furthestPositionInRecord = furthestAfter;
291     return true;
292   } else {
293     handler.SignalEnd();
294     endfileRecordNumber = currentRecordNumber;
295     return false;
296   }
297 }
298 
299 std::optional<char32_t> ExternalFileUnit::GetCurrentChar(
300     IoErrorHandler &handler) {
301   RUNTIME_CHECK(handler, direction_ == Direction::Input);
302   if (const char *p{FrameNextInput(handler, 1)}) {
303     // TODO: UTF-8 decoding; may have to get more bytes in a loop
304     return *p;
305   }
306   return std::nullopt;
307 }
308 
309 const char *ExternalFileUnit::FrameNextInput(
310     IoErrorHandler &handler, std::size_t bytes) {
311   RUNTIME_CHECK(handler, !isUnformatted);
312   if (static_cast<std::int64_t>(positionInRecord + bytes) <=
313       recordLength.value_or(positionInRecord + bytes)) {
314     auto at{recordOffsetInFrame_ + positionInRecord};
315     auto need{static_cast<std::size_t>(at + bytes)};
316     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
317     SetSequentialVariableFormattedRecordLength();
318     if (got >= need) {
319       return Frame() + at;
320     }
321     handler.SignalEnd();
322     endfileRecordNumber = currentRecordNumber;
323   }
324   return nullptr;
325 }
326 
327 bool ExternalFileUnit::SetSequentialVariableFormattedRecordLength() {
328   if (recordLength || access != Access::Sequential) {
329     return true;
330   }
331   if (FrameLength() > recordOffsetInFrame_) {
332     const char *record{Frame() + recordOffsetInFrame_};
333     if (const char *nl{reinterpret_cast<const char *>(
334             std::memchr(record, '\n', FrameLength() - recordOffsetInFrame_))}) {
335       recordLength = nl - record;
336       if (*recordLength > 0 && record[*recordLength - 1] == '\r') {
337         --*recordLength;
338       }
339       return true;
340     }
341   }
342   return false;
343 }
344 
345 void ExternalFileUnit::SetLeftTabLimit() {
346   leftTabLimit = furthestPositionInRecord;
347   positionInRecord = furthestPositionInRecord;
348 }
349 
350 void ExternalFileUnit::BeginReadingRecord(IoErrorHandler &handler) {
351   RUNTIME_CHECK(handler, direction_ == Direction::Input);
352   if (access == Access::Sequential) {
353     if (endfileRecordNumber && currentRecordNumber >= *endfileRecordNumber) {
354       handler.SignalEnd();
355     } else if (isFixedRecordLength) {
356       RUNTIME_CHECK(handler, recordLength.has_value());
357       auto need{static_cast<std::size_t>(recordOffsetInFrame_ + *recordLength)};
358       auto got{ReadFrame(frameOffsetInFile_, need, handler)};
359       if (got < need) {
360         handler.SignalEnd();
361       }
362     } else if (isUnformatted) {
363       BeginSequentialVariableUnformattedInputRecord(handler);
364     } else { // formatted
365       BeginSequentialVariableFormattedInputRecord(handler);
366     }
367   }
368 }
369 
370 bool ExternalFileUnit::AdvanceRecord(IoErrorHandler &handler) {
371   bool ok{true};
372   if (direction_ == Direction::Input) {
373     if (access == Access::Sequential) {
374       RUNTIME_CHECK(handler, recordLength.has_value());
375       if (isFixedRecordLength) {
376         frameOffsetInFile_ += recordOffsetInFrame_ + *recordLength;
377         recordOffsetInFrame_ = 0;
378       } else if (isUnformatted) {
379         // Retain footer in frame for more efficient BACKSPACE
380         frameOffsetInFile_ += recordOffsetInFrame_ + *recordLength;
381         recordOffsetInFrame_ = sizeof(std::uint32_t);
382         recordLength.reset();
383       } else { // formatted
384         if (Frame()[recordOffsetInFrame_ + *recordLength] == '\r') {
385           ++recordOffsetInFrame_;
386         }
387         recordOffsetInFrame_ += *recordLength + 1;
388         RUNTIME_CHECK(handler, Frame()[recordOffsetInFrame_ - 1] == '\n');
389         recordLength.reset();
390       }
391     }
392   } else { // Direction::Output
393     if (!isUnformatted) {
394       if (isFixedRecordLength && recordLength) {
395         if (furthestPositionInRecord < *recordLength) {
396           WriteFrame(frameOffsetInFile_, *recordLength, handler);
397           std::memset(Frame() + recordOffsetInFrame_ + furthestPositionInRecord,
398               ' ', *recordLength - furthestPositionInRecord);
399         }
400       } else {
401         positionInRecord = furthestPositionInRecord;
402         ok &= Emit("\n", 1, 1, handler); // TODO: Windows CR+LF
403       }
404     }
405     frameOffsetInFile_ +=
406         recordOffsetInFrame_ + recordLength.value_or(furthestPositionInRecord);
407     recordOffsetInFrame_ = 0;
408     impliedEndfile_ = true;
409   }
410   ++currentRecordNumber;
411   BeginRecord();
412   return ok;
413 }
414 
415 void ExternalFileUnit::BackspaceRecord(IoErrorHandler &handler) {
416   if (access != Access::Sequential) {
417     handler.SignalError(IostatBackspaceNonSequential,
418         "BACKSPACE(UNIT=%d) on non-sequential file", unitNumber());
419   } else {
420     DoImpliedEndfile(handler);
421     --currentRecordNumber;
422     BeginRecord();
423     if (isFixedRecordLength) {
424       BackspaceFixedRecord(handler);
425     } else if (isUnformatted) {
426       BackspaceVariableUnformattedRecord(handler);
427     } else {
428       BackspaceVariableFormattedRecord(handler);
429     }
430   }
431 }
432 
433 void ExternalFileUnit::FlushIfTerminal(IoErrorHandler &handler) {
434   if (isTerminal()) {
435     Flush(handler);
436   }
437 }
438 
439 void ExternalFileUnit::Endfile(IoErrorHandler &handler) {
440   if (access != Access::Sequential) {
441     handler.SignalError(IostatEndfileNonSequential,
442         "ENDFILE(UNIT=%d) on non-sequential file", unitNumber());
443   } else if (!mayWrite()) {
444     handler.SignalError(IostatEndfileUnwritable,
445         "ENDFILE(UNIT=%d) on read-only file", unitNumber());
446   } else {
447     DoEndfile(handler);
448   }
449 }
450 
451 void ExternalFileUnit::Rewind(IoErrorHandler &handler) {
452   if (access == Access::Direct) {
453     handler.SignalError(IostatRewindNonSequential,
454         "REWIND(UNIT=%d) on non-sequential file", unitNumber());
455   } else {
456     DoImpliedEndfile(handler);
457     SetPosition(0);
458     currentRecordNumber = 1;
459     // TODO: reset endfileRecordNumber?
460   }
461 }
462 
463 void ExternalFileUnit::EndIoStatement() {
464   frameOffsetInFile_ += recordOffsetInFrame_;
465   recordOffsetInFrame_ = 0;
466   io_.reset();
467   u_.emplace<std::monostate>();
468   lock_.Drop();
469 }
470 
471 void ExternalFileUnit::BeginSequentialVariableUnformattedInputRecord(
472     IoErrorHandler &handler) {
473   std::int32_t header{0}, footer{0};
474   std::size_t need{recordOffsetInFrame_ + sizeof header};
475   std::size_t got{ReadFrame(frameOffsetInFile_, need, handler)};
476   // Try to emit informative errors to help debug corrupted files.
477   const char *error{nullptr};
478   if (got < need) {
479     if (got == recordOffsetInFrame_) {
480       handler.SignalEnd();
481     } else {
482       error = "Unformatted variable-length sequential file input failed at "
483               "record #%jd (file offset %jd): truncated record header";
484     }
485   } else {
486     std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header);
487     recordLength = sizeof header + header; // does not include footer
488     need = recordOffsetInFrame_ + *recordLength + sizeof footer;
489     got = ReadFrame(frameOffsetInFile_, need, handler);
490     if (got < need) {
491       error = "Unformatted variable-length sequential file input failed at "
492               "record #%jd (file offset %jd): hit EOF reading record with "
493               "length %jd bytes";
494     } else {
495       std::memcpy(&footer, Frame() + recordOffsetInFrame_ + *recordLength,
496           sizeof footer);
497       if (footer != header) {
498         error = "Unformatted variable-length sequential file input failed at "
499                 "record #%jd (file offset %jd): record header has length %jd "
500                 "that does not match record footer (%jd)";
501       }
502     }
503   }
504   if (error) {
505     handler.SignalError(error, static_cast<std::intmax_t>(currentRecordNumber),
506         static_cast<std::intmax_t>(frameOffsetInFile_),
507         static_cast<std::intmax_t>(header), static_cast<std::intmax_t>(footer));
508     // TODO: error recovery
509   }
510   positionInRecord = sizeof header;
511 }
512 
513 void ExternalFileUnit::BeginSequentialVariableFormattedInputRecord(
514     IoErrorHandler &handler) {
515   if (this == defaultInput && defaultOutput) {
516     defaultOutput->Flush(handler);
517   }
518   std::size_t length{0};
519   do {
520     std::size_t need{recordOffsetInFrame_ + length + 1};
521     length = ReadFrame(frameOffsetInFile_, need, handler);
522     if (length < need) {
523       handler.SignalEnd();
524       break;
525     }
526   } while (!SetSequentialVariableFormattedRecordLength());
527 }
528 
529 void ExternalFileUnit::BackspaceFixedRecord(IoErrorHandler &handler) {
530   RUNTIME_CHECK(handler, recordLength.has_value());
531   if (frameOffsetInFile_ < *recordLength) {
532     handler.SignalError(IostatBackspaceAtFirstRecord);
533   } else {
534     frameOffsetInFile_ -= *recordLength;
535   }
536 }
537 
538 void ExternalFileUnit::BackspaceVariableUnformattedRecord(
539     IoErrorHandler &handler) {
540   std::int32_t header{0}, footer{0};
541   auto headerBytes{static_cast<std::int64_t>(sizeof header)};
542   frameOffsetInFile_ += recordOffsetInFrame_;
543   recordOffsetInFrame_ = 0;
544   if (frameOffsetInFile_ <= headerBytes) {
545     handler.SignalError(IostatBackspaceAtFirstRecord);
546     return;
547   }
548   // Error conditions here cause crashes, not file format errors, because the
549   // validity of the file structure before the current record will have been
550   // checked informatively in NextSequentialVariableUnformattedInputRecord().
551   std::size_t got{
552       ReadFrame(frameOffsetInFile_ - headerBytes, headerBytes, handler)};
553   RUNTIME_CHECK(handler, got >= sizeof footer);
554   std::memcpy(&footer, Frame(), sizeof footer);
555   recordLength = footer;
556   RUNTIME_CHECK(handler, frameOffsetInFile_ >= *recordLength + 2 * headerBytes);
557   frameOffsetInFile_ -= *recordLength + 2 * headerBytes;
558   if (frameOffsetInFile_ >= headerBytes) {
559     frameOffsetInFile_ -= headerBytes;
560     recordOffsetInFrame_ = headerBytes;
561   }
562   auto need{static_cast<std::size_t>(
563       recordOffsetInFrame_ + sizeof header + *recordLength)};
564   got = ReadFrame(frameOffsetInFile_, need, handler);
565   RUNTIME_CHECK(handler, got >= need);
566   std::memcpy(&header, Frame() + recordOffsetInFrame_, sizeof header);
567   RUNTIME_CHECK(handler, header == *recordLength);
568 }
569 
570 // There's no portable memrchr(), unfortunately, and strrchr() would
571 // fail on a record with a NUL, so we have to do it the hard way.
572 static const char *FindLastNewline(const char *str, std::size_t length) {
573   for (const char *p{str + length}; p-- > str;) {
574     if (*p == '\n') {
575       return p;
576     }
577   }
578   return nullptr;
579 }
580 
581 void ExternalFileUnit::BackspaceVariableFormattedRecord(
582     IoErrorHandler &handler) {
583   // File offset of previous record's newline
584   auto prevNL{
585       frameOffsetInFile_ + static_cast<std::int64_t>(recordOffsetInFrame_) - 1};
586   if (prevNL < 0) {
587     handler.SignalError(IostatBackspaceAtFirstRecord);
588     return;
589   }
590   while (true) {
591     if (frameOffsetInFile_ < prevNL) {
592       if (const char *p{
593               FindLastNewline(Frame(), prevNL - 1 - frameOffsetInFile_)}) {
594         recordOffsetInFrame_ = p - Frame() + 1;
595         *recordLength = prevNL - (frameOffsetInFile_ + recordOffsetInFrame_);
596         break;
597       }
598     }
599     if (frameOffsetInFile_ == 0) {
600       recordOffsetInFrame_ = 0;
601       *recordLength = prevNL;
602       break;
603     }
604     frameOffsetInFile_ -= std::min<std::int64_t>(frameOffsetInFile_, 1024);
605     auto need{static_cast<std::size_t>(prevNL + 1 - frameOffsetInFile_)};
606     auto got{ReadFrame(frameOffsetInFile_, need, handler)};
607     RUNTIME_CHECK(handler, got >= need);
608   }
609   RUNTIME_CHECK(handler, Frame()[recordOffsetInFrame_ + *recordLength] == '\n');
610   if (*recordLength > 0 &&
611       Frame()[recordOffsetInFrame_ + *recordLength - 1] == '\r') {
612     --*recordLength;
613   }
614 }
615 
616 void ExternalFileUnit::DoImpliedEndfile(IoErrorHandler &handler) {
617   if (impliedEndfile_) {
618     impliedEndfile_ = false;
619     if (access == Access::Sequential && mayPosition()) {
620       DoEndfile(handler);
621     }
622   }
623 }
624 
625 void ExternalFileUnit::DoEndfile(IoErrorHandler &handler) {
626   endfileRecordNumber = currentRecordNumber;
627   Truncate(frameOffsetInFile_ + recordOffsetInFrame_, handler);
628   BeginRecord();
629   impliedEndfile_ = false;
630 }
631 } // namespace Fortran::runtime::io
632