// 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\nyields 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\rtoo, 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 ptseditor_open(e, path)โ loadpathinto an initialized editor, replacing its contents. Missing file: succeed with one empty line. Strip\nterminators and any preceding\r. Copypathintoe->filename. Cleardirty. 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. Updatee->filename, cleardirty. 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.