// 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 pts

Implement the load/save text transforms:

  • load_text(text) โ†’ LoadResult{lines, crlf, trailing_newline}:
    • Split on \n; a \r immediately before a \n is not part of the line's content.
    • crlf is 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_newline is 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 \r not followed by \n is ordinary content (classic-Mac files are 25 years dead; we keep the byte rather than guess).
  • save_text(lines, crlf, trailing_newline): join with "\r\n" or "\n", append a final break iff trailing_newline. Must be the exact inverse: save_text of a load_text reproduces the original bytes for any input without stray \rs.

Log in to submit a solution and earn points.