// lesson: file-io

Loading and Saving Files

An editor that can't open and save files is a very elaborate toy. This lesson is short on new syscalls โ€” you know open, read, write โ€” and long on the judgment calls, because file I/O is where an editor can destroy the user's data, and the difference between "editor" and "data shredder" is a handful of decisions made correctly.

Loading: bytes โ†’ lines

Loading is a decode step: the file is a flat byte string; your buffer is an array of lines. Split on '\n', with three conventions:

  • The trailing newline belongs to the format, not the text. A well-formed Unix text file ends with \n ("a\nb\n" is the two-line file). Naively splitting on every \n yields a phantom third empty line; strip the terminator instead of storing it.
  • Tolerate a missing final newline. "a\nb" is technically malformed but everywhere; load it as two lines. (Then fix it on save โ€” see below.)
  • Tolerate CRLF. Files that crossed a Windows machine end lines with \r\n; strip the \r too, or every line of the file wears an invisible last character that makes end-of-line navigation feel haunted.

A file the OS says doesn't exist (ENOENT) is not an error for an editor: vim newfile.txt opens an empty buffer and creates the file on first save. Any other open failure (EACCES, EISDIRโ€ฆ) is a real error to report.

Saving: the decision that matters

Serializing is trivial โ€” every line, '\n' after each, done (quietly repairing any missing final newline). The question is how the bytes reach the disk. The obvious way:

fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, everything, len);

Read it as a timeline and see the trap: O_TRUNC destroys the old contents at open, before one byte of the new contents is written. Crash between those steps โ€” power loss, OOM-kill, a bug in your own editor โ€” and the user's file is now zero bytes. Their old data is gone and their new data is gone. Editors have shipped this; users remember.

The professional pattern is write-temp-then-rename:

fd = open("file.txt.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write_all(fd, everything, len);   /* your helper from lesson 1 */
close(fd);
rename("file.txt.tmp", "file.txt");

The load-bearing fact is that POSIX rename(2) is atomic: at every instant, file.txt is either entirely the old file or entirely the new one. There is no moment when it's empty or half written. A crash before the rename leaves the old file untouched (plus a stray .tmp to clean up); after, the new file is complete. This one idiom protects more user data than any amount of testing.

(Going further โ€” fsync before the rename to survive power loss with certainty, preserving ownership/permissions of the original, keeping the temp file on the same filesystem so rename stays atomic โ€” is real and matters for production editors; the shape stays exactly this.)

One more piece of editor state rides along with saving: the dirty flag โ€” set by every edit, cleared by a successful save, consulted by "quit without saving?". Wire it now: editor_open and editor_save both end with e->dirty = 0.

โ€บ Load and Save

25 pts
  • editor_open(e, path) โ€” load path into an initialized editor, replacing its contents. Missing file: succeed with one empty line. Strip \n terminators and any preceding \r. Copy path into e->filename. Clear dirty. Return 0; -1 on real errors (in which case the editor must still be in a valid state).
  • editor_save(e, path) โ€” write all lines, '\n' after each, via a temp file ("<path>.tmp") renamed over the target. Update e->filename, clear dirty. Return the number of bytes written, -1 on error.

The tests round-trip files through a real filesystem, cover the trailing-newline and CRLF conventions, verify the temp file doesn't linger, and โ€” the tell for O_TRUNC-style saving โ€” check that saving over an existing file replaces it completely.

Log in to submit a solution and earn points.