// lesson: files-and-saving
Files โ Open, Save, Don't Lose Data
An editor's one sacred duty: never lose the user's work. Loading is easy; saving is where the sacred duty meets the operating system.
Loading, and the line-ending question
Read the whole file (an editor's working set is the whole file anyway),
then split into the line vector. Splitting meets the oldest portability
scar in computing: Unix ends lines with \n (LF); Windows with \r\n
(CR LF) โ a literal carriage-return-then-line-feed, teletype choreography
that outlived the teletype by half a century. Open a Windows file naively
and every line grows a trailing \r that renders as nothing, breaks
end-of-line motions, and pollutes every diff if you save.
The grown-up policy (vim's, roughly): detect the convention from the
file's first line break, normalize in memory (lines never contain
line terminators), and restore the convention on save. Two more
facts to preserve: whether the file ended with a final newline (POSIX
says a text file's last line ends with one, and build tools care โ
editors that silently add or drop it create one-line diffs), and โ for
the buffer invariant โ an empty file still becomes {""}.
While we're at file edges: what if stdin isn't the terminal at all โ
git diff | vim -? The editor reads the file from stdin, then opens
/dev/tty โ a magic path that always names the process's
controlling terminal โ to get a keyboard back. That's the tool for
"I need the terminal even though my fds are redirected"; the isatty(0)
check from Lesson 1 is how you notice you're in that situation.
Saving: the rename trick
The naive save โ open the file with O_TRUNC, write the buffer โ has a
window of doom: after the truncate, before the write completes, the file
on disk is empty or partial. Crash there (power, OOM-kill, a full
disk mid-write) and the user's file is gone. The fix is one of the great
POSIX idioms, write-temp-then-rename:
// 1. write the full contents to a temp file ON THE SAME DIRECTORY
int fd = open(".notes.txt.tmp", O_WRONLY | O_CREAT | O_EXCL, 0644);
write(fd, data, len); // (in a loop; write can be partial)
fsync(fd); // 2. force it to stable storage
close(fd);
rename(".notes.txt.tmp", "notes.txt"); // 3. atomic replace
rename(2) is atomic: any observer โ and any crash โ sees either
the old complete file or the new complete file, never a mixture, never
an absence. The fsync before it matters just as much: without it the
rename can hit disk before the data does, and a crash leaves you a
perfectly renamed empty file. Same-directory matters too โ rename
can't cross filesystems (EXDEV), which is why the temp file lives next
to the target, not in /tmp. (The costs: it breaks hard links and needs
a re-chown/chmod for exotic permissions โ vim exposes this whole
tradeoff as the backupcopy option. No free lunch, but the default is
clear.)
The dirty flag drives the UX around all this: set on every buffer
edit, cleared on save, consulted by quit ("unsaved changes! press Ctrl-Q
again to discard"), displayed as the [+] your status bar already
renders. It's a one-bit summary of "does the buffer differ from disk" โ
cheap because it can only go stale in the safe direction (an edit
followed by its exact inverse still reads dirty; annoying, never
dangerous).
The syscalls are your editor's job; the graded core is the codec โ the detect/normalize/restore logic, plus round-trip fidelity, which is where line-ending bugs actually live.
โบ The Line Codec
15 ptsImplement the load/save text transforms:
load_text(text)โLoadResult{lines, crlf, trailing_newline}:- Split on
\n; a\rimmediately before a\nis not part of the line's content. crlfis true iff the first line break in the text is\r\n(that convention is then assumed for the whole file, vim-style). No line breaks โ false.trailing_newlineis true iff the text ends with a line break; that final break does not produce an extra empty line.- Empty text โ
{{""}, false, false}(the buffer invariant). - A
\rnot followed by\nis ordinary content (classic-Mac files are 25 years dead; we keep the byte rather than guess).
- Split on
save_text(lines, crlf, trailing_newline): join with"\r\n"or"\n", append a final break ifftrailing_newline. Must be the exact inverse:save_textof aload_textreproduces the original bytes for any input without stray\rs.
Log in to submit a solution and earn points.