K&R2 Exercise 8-4. The standard library function

2026-10-21

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
//compile with: gcc -Wall -Wextra cc-c.c -o cc-c
//run with:     ./cc-c < zhouyi.txt         # stdin is a file: section 8 can seek it
//              echo 乾為天 | ./cc-c        # stdin is a pipe: section 8 can't
//              ./cc-c                      # stdin is the terminal: can't either

//K&R2 Exercise 8-4. The standard library function
//    int fseek(FILE *fp, long offset, int origin)
//is identical to lseek except that fp is a file pointer instead of a
//file descriptor and the return value is an int status, not a
//position. Write fseek. Make sure that your fseek coordinates properly
//with the buffering done for the other functions of the library.
//
//In plain words: fseek moves a file's position, like lseek does for an
//fd; it returns 0 if that worked, EOF if not. The catch is the buffer.
//With a buffer there are TWO positions: the kernel's (where the next
//read() or write() happens) and the program's (where the next getc or
//putc happens). Plain lseek moves only the kernel's:
//  reading: the kernel has read a whole buffer ahead -- the unread
//           bytes left in it must be thrown away, and origin 1 ("from
//           here") must mean from where the PROGRAM is
//  writing: the buffer holds bytes meant for the OLD position -- write
//           them out before moving
//
//The 8.5 and 8-3 code is here unchanged (my_ / M_ names); new:
//my_fseek, naive_fseek (plain lseek, to show what goes wrong), and
//my_ftell (where the program is -- ftell, not asked for, used by the
//printfs). main:
//  1. the two positions, after 3 getc
//  2. naive_fseek: back 3, and the file comes out 3 bytes too long
//  3. my_fseek: back to the start, back 3, the count comes out right
//  4. origin 2: from the end -- the last line of zhouyi.txt
//  5. fseek clears EOF; and the cnt < 0 trap left over from 8.5
//  6. writing: my_fseek flushes first; naive_fseek scrambles the file
//  7. a failed fseek leaves everything as it was
//  8. stdin: a file can seek, a pipe or terminal can't
//  9. fopen "a" is open + lseek here; the real one is O_APPEND

#define MY_OPEN_MAX 20
#define MY_BUFSIZ   1024
#define PERMS       0666
#define DEMO        "seek-demo.txt"     //made and unlinked by main

typedef struct my_iobuf {
    int   cnt;      //reading: characters left; writing: space left
    char *ptr;      //next character position
    char *base;     //location of buffer
    int   flag;     //mode of file access
    int   fd;       //file descriptor
} MYFILE;

enum my_flags {
    M_READ  = 01,
    M_WRITE = 02,
    M_UNBUF = 04,
    M_EOF   = 010,
    M_ERR   = 020
};

MYFILE my_iob[MY_OPEN_MAX] = {
    { 0, (char *) 0, (char *) 0, M_READ, 0 },
    { 0, (char *) 0, (char *) 0, M_WRITE, 1 },
    { 0, (char *) 0, (char *) 0, M_WRITE | M_UNBUF, 2 }
};

#define my_stdin  (&my_iob[0])
#define my_stdout (&my_iob[1])
#define my_stderr (&my_iob[2])

int my_fillbuf(MYFILE *);
int my_flushbuf(int, MYFILE *);
int my_fflush(MYFILE *);

#define my_feof(p)   (((p)->flag & M_EOF) != 0)
#define my_ferror(p) (((p)->flag & M_ERR) != 0)

#define my_getc(p) (--(p)->cnt >= 0 \
                   ? (unsigned char) *(p)->ptr++ : my_fillbuf(p))
//the book's putc, from 8.5. For writing, cnt counts SPACE left: while
//there is room, store x and move on; when cnt goes below 0, the buffer
//is full and _flushbuf takes over
#define my_putc(x,p) (--(p)->cnt >= 0 \
                     ? *(p)->ptr++ = (x) : my_flushbuf((x),p))
#define my_putchar(x) my_putc((x), my_stdout)

int nwrites = 0;    //not in the book: how many times write() was called

//my_fopen:  open file, return file ptr -- 8.5, unchanged
MYFILE *my_fopen(char *name, char *mode)
{
    int fd;
    MYFILE *fp;

    if (*mode != 'r' && *mode != 'w' && *mode != 'a')
        return NULL;
    for (fp = my_iob; fp < my_iob + MY_OPEN_MAX; fp++)
        if ((fp->flag & (M_READ | M_WRITE)) == 0)
            break;
    if (fp >= my_iob + MY_OPEN_MAX)
        return NULL;

    if (*mode == 'w')
        fd = creat(name, PERMS);
    else if (*mode == 'a') {
        if ((fd = open(name, O_WRONLY, 0)) == -1)
            fd = creat(name, PERMS);
        lseek(fd, 0L, 2);
    } else
        fd = open(name, O_RDONLY, 0);
    if (fd == -1)
        return NULL;
    fp->fd = fd;
    fp->cnt = 0;
    fp->base = NULL;
    fp->flag = (*mode == 'r') ? M_READ : M_WRITE;
    return fp;
}

//my_fillbuf:  allocate and fill input buffer -- 8.5, unchanged
int my_fillbuf(MYFILE *fp)
{
    int bufsize;

    if ((fp->flag & (M_READ | M_EOF | M_ERR)) != M_READ)
        return EOF;
    bufsize = (fp->flag & M_UNBUF) ? 1 : MY_BUFSIZ;
    if (fp->base == NULL)
        if ((fp->base = (char *) malloc(bufsize)) == NULL)
            return EOF;
    fp->ptr = fp->base;
    fp->cnt = read(fp->fd, fp->ptr, bufsize);
    if (--fp->cnt < 0) {
        if (fp->cnt == -1)
            fp->flag |= M_EOF;
        else
            fp->flag |= M_ERR;
        fp->cnt = 0;
        return EOF;
    }
    return (unsigned char) *fp->ptr++;
}

//=== 8-3: _flushbuf, fflush, fclose -- unchanged ===================

//my_flushbuf:  allocate or flush output buffer, then store c in it
int my_flushbuf(int c, MYFILE *fp)
{
    unsigned char uc = c;

    //IDIOM: (flag & (A|B)) != A -- "A set, B clear", the _fillbuf test
    //for writing: open for writing, and no error so far
    if ((fp->flag & (M_WRITE | M_ERR)) != M_WRITE)
        return EOF;
    //DESIGN: unbuffered means no buffer at all -- write c right now.
    //(Giving it a 1-byte buffer, like _fillbuf does, would leave every
    //character sitting there until the NEXT putc: stderr would always
    //be one character behind)
    if (fp->flag & M_UNBUF) {
        nwrites++;                          //not in the book
        if (write(fp->fd, &uc, 1) != 1) {
            fp->flag |= M_ERR;
            return EOF;
        }
        fp->cnt = 0;        //so the next putc comes straight back here
        return uc;
    }
    //DESIGN: first time, get a buffer; after that, a full buffer is
    //exactly what fflush empties -- so call it, don't write it twice
    if (fp->base == NULL) {
        if ((fp->base = (char *) malloc(MY_BUFSIZ)) == NULL) {
            fp->flag |= M_ERR;
            return EOF;
        }
    } else if (my_fflush(fp) == EOF)
        return EOF;
    fp->ptr = fp->base;
    *fp->ptr++ = uc;
    fp->cnt = MY_BUFSIZ - 1;    //one place used by c
    //DESIGN: return c as unsigned char, like _fillbuf -- so byte 0xff
    //is 255 here, never EOF (section 10: the macro's other path isn't)
    return uc;
}

//my_fflush:  write out a file's buffered output; NULL means all files
int my_fflush(MYFILE *fp)
{
    int n, rc;

    //DESIGN: fflush(NULL) flushes every file open for writing, as in
    //Appendix B; one failure makes the whole call return EOF, but the
    //loop still tries every file
    if (fp == NULL) {
        rc = 0;
        for (fp = my_iob; fp < my_iob + MY_OPEN_MAX; fp++)
            if ((fp->flag & M_WRITE) && my_fflush(fp) == EOF)
                rc = EOF;
        return rc;
    }
    //DESIGN: on a file open for reading the standard says the effect is
    //undefined; here it does nothing and succeeds. A slot that isn't
    //open at all is an error
    if ((fp->flag & M_WRITE) == 0)
        return (fp->flag & M_READ) ? 0 : EOF;
    if (fp->flag & M_ERR)
        return EOF;
    //IDIOM: ptr - base -- two pointers into one buffer: subtracting
    //them counts the characters stored so far
    if (fp->base != NULL && (n = fp->ptr - fp->base) > 0) {
        nwrites++;                          //not in the book
        if (write(fp->fd, fp->base, n) != n) {
            fp->flag |= M_ERR;
            return EOF;
        }
    }
    fp->ptr = fp->base;
    //no buffer yet (or unbuffered): cnt 0, so the next putc calls
    //_flushbuf; otherwise the whole buffer is free again
    fp->cnt = (fp->base == NULL) ? 0 : MY_BUFSIZ;
    return 0;
}

//my_fclose:  flush, free the buffer, close the fd, free the slot
int my_fclose(MYFILE *fp)
{
    int rc = 0;

    //DESIGN: refuse anything that isn't an open slot of the table --
    //NULL, a stray pointer, or a file already closed
    if (fp == NULL || fp < my_iob || fp >= my_iob + MY_OPEN_MAX
            || (fp->flag & (M_READ | M_WRITE)) == 0)
        return EOF;
    //DESIGN: a failed flush or close still frees the slot -- the file
    //is closed either way; the EOF tells the caller data may be lost
    if ((fp->flag & M_WRITE) && my_fflush(fp) == EOF)
        rc = EOF;
    if (close(fp->fd) == -1)
        rc = EOF;
    //IDIOM: free(NULL) is allowed and does nothing -- no need to test
    //base first
    free(fp->base);
    fp->base = fp->ptr = NULL;
    fp->cnt = 0;
    fp->flag = 0;       //flag 0: the slot is free for my_fopen again
    fp->fd = -1;
    return rc;
}


//=== the exercise ======================================================

//my_fseek:  move fp to offset from origin (0 start, 1 here, 2 end);
//return 0, or EOF if it can't
int my_fseek(MYFILE *fp, long offset, int origin)
{
    if ((fp->flag & (M_READ | M_WRITE)) == 0)
        return EOF;         //not an open file
    if (fp->flag & M_WRITE) {
        //DESIGN: writing -- the buffered bytes belong at the OLD
        //position: write them there first, then move
        if (my_fflush(fp) == EOF)
            return EOF;
    } else if (origin == 1 && fp->cnt > 0)
        //DESIGN: reading -- the kernel is cnt bytes ahead of the
        //program, so "from here" starts cnt bytes back.
        //The cnt > 0 test: after EOF, each extra getc leaves cnt one
        //lower (-1, -2 ... section 5 of 8.5); those aren't bytes
        offset -= fp->cnt;
    //DESIGN: lseek BEFORE touching the read buffer: if it fails, the
    //position hasn't moved and the buffer still matches it
    if (lseek(fp->fd, offset, origin) == -1)
        return EOF;
    if (fp->flag & M_READ) {
        fp->cnt = 0;            //drop the buffered bytes: next getc refills
        //IDIOM: flag &= ~A -- clear one bit, keep the rest
        fp->flag &= ~M_EOF;     //the standard: fseek clears end-of-file
    }
    return 0;
}

//=== not in the book: for main =========================================

//the obvious fseek, that forgets the buffer
int naive_fseek(MYFILE *fp, long offset, int origin)
{
    return (lseek(fp->fd, offset, origin) == -1) ? EOF : 0;
}

//my_ftell:  the program's position = the kernel's, corrected by the
//buffer: minus the unread bytes (reading), plus the unwritten (writing)
long my_ftell(MYFILE *fp)
{
    long k = lseek(fp->fd, 0L, 1);      //offset 0 from here: just ask

    if (k == -1)
        return -1;
    if (fp->flag & M_READ)
        return (fp->cnt > 0) ? k - fp->cnt : k;
    return (fp->base != NULL) ? k + (fp->ptr - fp->base) : k;
}

long size_on_disk(char *name)
{
    struct stat st;

    return (stat(name, &st) == -1) ? -1L : (long) st.st_size;
}

//read up to n-1 characters with my_getc into s, as a string
char *getn(MYFILE *fp, char *s, int n)
{
    int c, i;

    for (i = 0; i < n - 1 && (c = my_getc(fp)) != EOF; i++)
        s[i] = c;
    s[i] = '\0';
    return s;
}

//count the characters from here to EOF
long count_rest(MYFILE *fp)
{
    long n = 0;

    while (my_getc(fp) != EOF)
        n++;
    return n;
}

//a small file's whole contents, as a string; a 0 byte is shown as \0
char *contents(char *name, char *s, int n)
{
    int fd, i, j, k;
    char raw[100];

    if ((fd = open(name, O_RDONLY, 0)) == -1)
        return strcpy(s, "?");
    k = read(fd, raw, sizeof(raw));
    close(fd);
    for (i = j = 0; i < k && j < n - 3; i++)
        if (raw[i] == '\0') {
            s[j++] = '\\';
            s[j++] = '0';
        } else
            s[j++] = raw[i];
    s[j] = '\0';
    return s;
}

int main(void)
{
    MYFILE *fp, *wp;
    FILE *rf;
    int i, r;
    long n;
    char s[200], t[200];

    //--- 1. two positions ----------------------------------------------
    fp = my_fopen("zhouyi.txt", "r");
    getn(fp, s, 4);
    printf("1. 3 my_getc: \"%s\" (3 bytes of UTF-8)\n", s);
    printf("   program at %ld (my_ftell), kernel at %ld (lseek), %d bytes waiting\n\n",
           my_ftell(fp), (long) lseek(fp->fd, 0L, 1), fp->cnt);
    my_fclose(fp);

    //--- 2. naive ------------------------------------------------------
    fp = my_fopen("zhouyi.txt", "r");
    getn(fp, s, 4);
    naive_fseek(fp, -3L, 1);
    printf("2. naive_fseek(fp, -3, 1), meant \"back to 厥\": next 3 bytes \"%s\"\n",
           getn(fp, s, 4));
    printf("   the kernel went from 1024 back to 1021; the buffer didn't notice\n");
    n = 6 + count_rest(fp);
    printf("   reading on to EOF: %ld bytes in all, the file has %ld -- the next\n"
           "   refill started at 1021, so 3 bytes came twice\n\n",
           n, size_on_disk("zhouyi.txt"));
    my_fclose(fp);

    //--- 3. my_fseek ---------------------------------------------------
    fp = my_fopen("zhouyi.txt", "r");
    getn(fp, s, 4);
    r = my_fseek(fp, -3L, 1);
    //one printf per state: getn changes cnt and the position, and the
    //order printf's arguments are evaluated in isn't fixed
    printf("3. my_fseek(fp, -3, 1) -> %d: kernel to %ld (1024 - 1021 waiting - 3), cnt %d\n",
           r, (long) lseek(fp->fd, 0L, 1), fp->cnt);
    printf("   next 3 bytes \"%s\"\n", getn(fp, s, 4));
    n = 3 + count_rest(fp);
    printf("   reading on to EOF: %ld bytes in all, right\n", n);
    r = my_fseek(fp, 0L, 0);
    printf("   my_fseek(fp, 0, 0) -> %d: \"%s\" -- the start again\n\n",
           r, getn(fp, s, 4));

    //--- 4. from the end -----------------------------------------------
    //zhouyi.txt ends with its last line, then 7 empty lines (\r\n each):
    //the line starts 34 bytes before the end
    r = my_fseek(fp, -34L, 2);
    getn(fp, s, 40);
    s[strcspn(s, "\r")] = '\0';     //cut at the \r: the line alone
    printf("4. my_fseek(fp, -34, 2) -> %d: \"%s\" -- the last hexagram\n\n", r, s);

    //--- 5. EOF --------------------------------------------------------
    count_rest(fp);
    my_getc(fp);
    my_getc(fp);
    printf("5. read to EOF, then 2 more my_getc: feof %d, cnt %d, my_ftell %ld\n",
           my_feof(fp), fp->cnt, my_ftell(fp));
    r = my_fseek(fp, 0L, 1);
    printf("   my_fseek(fp, 0, 1) -> %d: kernel at %ld, not %ld -- the cnt > 0\n"
           "   test kept -2 from being taken as \"2 bytes ahead\"\n",
           r, (long) lseek(fp->fd, 0L, 1), size_on_disk("zhouyi.txt") + 2);
    printf("   feof now %d", my_feof(fp));
    printf(": so my_getc reads again, and gets %d (still the end)\n\n", my_getc(fp));
    my_fclose(fp);

    //--- 6. writing ----------------------------------------------------
    wp = my_fopen(DEMO, "w");
    for (i = 0; i < 10; i++)
        my_putc('0' + i, wp);
    printf("6. \"0123456789\" with my_putc: %ld bytes on disk, 10 in the buffer\n",
           size_on_disk(DEMO));
    r = my_fseek(wp, 2L, 0);
    printf("   my_fseek(wp, 2, 0) -> %d: %ld bytes on disk -- flushed first\n",
           r, size_on_disk(DEMO));
    my_putc('X', wp);
    my_putc('Y', wp);
    my_fclose(wp);
    printf("   2 my_putc, my_fclose: \"%s\"\n", contents(DEMO, s, sizeof(s)));
    wp = my_fopen(DEMO, "w");
    for (i = 0; i < 10; i++)
        my_putc('0' + i, wp);
    naive_fseek(wp, 2L, 0);
    my_putc('X', wp);
    my_putc('Y', wp);
    my_fclose(wp);
    printf("   the same with naive_fseek: \"%s\" (%ld bytes)\n",
           contents(DEMO, s, sizeof(s)), size_on_disk(DEMO));
    printf("   the digits were still in the buffer when lseek moved to 2, so all\n"
           "   12 bytes went out at close, starting at 2 -- and the 2 bytes never\n"
           "   written read back as zeros: a hole\n\n");

    //--- 7. a failed fseek -------------------------------------------
    fp = my_fopen("zhouyi.txt", "r");
    getn(fp, s, 4);
    r = my_fseek(fp, -10L, 0);
    printf("7. my_fseek(fp, -10, 0) -> %d: a position before the start\n", r);
    r = my_fseek(fp, 0L, 7);
    printf("   my_fseek(fp, 0, 7) -> %d: no origin 7\n", r);
    printf("   still at %ld, cnt %d", my_ftell(fp), fp->cnt);
    printf(": next 3 bytes \"%s\" as if nothing happened\n", getn(fp, s, 4));
    my_fclose(fp);
    printf("   my_fseek on it after my_fclose -> %d\n\n", my_fseek(fp, 0L, 0));

    //--- 8. stdin ------------------------------------------------------
    getn(my_stdin, s, 4);
    r = my_fseek(my_stdin, 0L, 0);
    if (r == 0)
        printf("8. stdin: read \"%s\", my_fseek(my_stdin, 0, 0) -> 0, read \"%s\"\n"
               "   again: stdin is a file, and a file has positions\n\n",
               s, getn(my_stdin, t, 4));
    else
        printf("8. stdin: my_fseek(my_stdin, 0, 0) -> %d: a pipe or a terminal has\n"
               "   no positions -- bytes go by once, and lseek fails\n\n", r);

    //--- 9. append ---------------------------------------------------
    wp = my_fopen(DEMO, "w");
    for (i = 0; i < 10; i++)
        my_putc('0' + i, wp);
    my_fclose(wp);
    wp = my_fopen(DEMO, "a");
    my_fseek(wp, 0L, 0);
    my_putc('A', wp);
    my_fclose(wp);
    printf("9. \"0123456789\", my_fopen \"a\", my_fseek(wp, 0, 0), my_putc('A'):\n"
           "   \"%s\" -- the A landed at the start\n", contents(DEMO, s, sizeof(s)));
    rf = fopen(DEMO, "a");
    fseek(rf, 0L, SEEK_SET);
    putc('B', rf);
    fclose(rf);
    printf("   the real fopen \"a\", fseek(rf, 0, SEEK_SET), putc('B'):\n"
           "   \"%s\" -- glibc opens with O_APPEND: every write() goes to the\n"
           "   end, wherever the position is\n", contents(DEMO, s, sizeof(s)));
    unlink(DEMO);
    return 0;
}

win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra cc-c.c -o cc-c
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c < zhouyi.txt
1. 3 my_getc: "厥" (3 bytes of UTF-8)
   program at 3 (my_ftell), kernel at 1024 (lseek), 1021 bytes waiting

2. naive_fseek(fp, -3, 1), meant "back to 厥": next 3 bytes "孚"
   the kernel went from 1024 back to 1021; the buffer didn't notice
   reading on to EOF: 264392 bytes in all, the file has 264389 -- the next
   refill started at 1021, so 3 bytes came twice

3. my_fseek(fp, -3, 1) -> 0: kernel to 0 (1024 - 1021 waiting - 3), cnt 0
   next 3 bytes "厥"
   reading on to EOF: 264389 bytes in all, right
   my_fseek(fp, 0, 0) -> 0: "厥" -- the start again

4. my_fseek(fp, -34, 2) -> 0: "64 未濟 (wèi jì)" -- the last hexagram

5. read to EOF, then 2 more my_getc: feof 1, cnt -3, my_ftell 264389
   my_fseek(fp, 0, 1) -> 0: kernel at 264389, not 264391 -- the cnt > 0
   test kept -2 from being taken as "2 bytes ahead"
   feof now 0: so my_getc reads again, and gets -1 (still the end)

6. "0123456789" with my_putc: 0 bytes on disk, 10 in the buffer
   my_fseek(wp, 2, 0) -> 0: 10 bytes on disk -- flushed first
   2 my_putc, my_fclose: "01XY456789"
   the same with naive_fseek: "\0\00123456789XY" (14 bytes)
   the digits were still in the buffer when lseek moved to 2, so all
   12 bytes went out at close, starting at 2 -- and the 2 bytes never
   written read back as zeros: a hole

7. my_fseek(fp, -10, 0) -> -1: a position before the start
   my_fseek(fp, 0, 7) -> -1: no origin 7
   still at 3, cnt 1021: next 3 bytes "孚" as if nothing happened
   my_fseek on it after my_fclose -> -1

8. stdin: read "厥", my_fseek(my_stdin, 0, 0) -> 0, read "厥"
   again: stdin is a file, and a file has positions

9. "0123456789", my_fopen "a", my_fseek(wp, 0, 0), my_putc('A'):
   "A123456789" -- the A landed at the start
   the real fopen "a", fseek(rf, 0, SEEK_SET), putc('B'):
   "A123456789B" -- glibc opens with O_APPEND: every write() goes to the
   end, wherever the position is
win@DESKTOP-MEIH88T:~/webdev-projects$ echo 乾為天 | ./cc-c
1. 3 my_getc: "厥" (3 bytes of UTF-8)
   program at 3 (my_ftell), kernel at 1024 (lseek), 1021 bytes waiting

2. naive_fseek(fp, -3, 1), meant "back to 厥": next 3 bytes "孚"
   the kernel went from 1024 back to 1021; the buffer didn't notice
   reading on to EOF: 264392 bytes in all, the file has 264389 -- the next
   refill started at 1021, so 3 bytes came twice

3. my_fseek(fp, -3, 1) -> 0: kernel to 0 (1024 - 1021 waiting - 3), cnt 0
   next 3 bytes "厥"
   reading on to EOF: 264389 bytes in all, right
   my_fseek(fp, 0, 0) -> 0: "厥" -- the start again

4. my_fseek(fp, -34, 2) -> 0: "64 未濟 (wèi jì)" -- the last hexagram

5. read to EOF, then 2 more my_getc: feof 1, cnt -3, my_ftell 264389
   my_fseek(fp, 0, 1) -> 0: kernel at 264389, not 264391 -- the cnt > 0
   test kept -2 from being taken as "2 bytes ahead"
   feof now 0: so my_getc reads again, and gets -1 (still the end)

6. "0123456789" with my_putc: 0 bytes on disk, 10 in the buffer
   my_fseek(wp, 2, 0) -> 0: 10 bytes on disk -- flushed first
   2 my_putc, my_fclose: "01XY456789"
   the same with naive_fseek: "\0\00123456789XY" (14 bytes)
   the digits were still in the buffer when lseek moved to 2, so all
   12 bytes went out at close, starting at 2 -- and the 2 bytes never
   written read back as zeros: a hole

7. my_fseek(fp, -10, 0) -> -1: a position before the start
   my_fseek(fp, 0, 7) -> -1: no origin 7
   still at 3, cnt 1021: next 3 bytes "孚" as if nothing happened
   my_fseek on it after my_fclose -> -1

8. stdin: my_fseek(my_stdin, 0, 0) -> -1: a pipe or a terminal has
   no positions -- bytes go by once, and lseek fails

9. "0123456789", my_fopen "a", my_fseek(wp, 0, 0), my_putc('A'):
   "A123456789" -- the A landed at the start
   the real fopen "a", fseek(rf, 0, SEEK_SET), putc('B'):
   "A123456789B" -- glibc opens with O_APPEND: every write() goes to the
   end, wherever the position is
win@DESKTOP-MEIH88T:~/webdev-projects$ ./cc-c
1. 3 my_getc: "厥" (3 bytes of UTF-8)
   program at 3 (my_ftell), kernel at 1024 (lseek), 1021 bytes waiting

2. naive_fseek(fp, -3, 1), meant "back to 厥": next 3 bytes "孚"
   the kernel went from 1024 back to 1021; the buffer didn't notice
   reading on to EOF: 264392 bytes in all, the file has 264389 -- the next
   refill started at 1021, so 3 bytes came twice

3. my_fseek(fp, -3, 1) -> 0: kernel to 0 (1024 - 1021 waiting - 3), cnt 0
   next 3 bytes "厥"
   reading on to EOF: 264389 bytes in all, right
   my_fseek(fp, 0, 0) -> 0: "厥" -- the start again

4. my_fseek(fp, -34, 2) -> 0: "64 未濟 (wèi jì)" -- the last hexagram

5. read to EOF, then 2 more my_getc: feof 1, cnt -3, my_ftell 264389
   my_fseek(fp, 0, 1) -> 0: kernel at 264389, not 264391 -- the cnt > 0
   test kept -2 from being taken as "2 bytes ahead"
   feof now 0: so my_getc reads again, and gets -1 (still the end)

6. "0123456789" with my_putc: 0 bytes on disk, 10 in the buffer
   my_fseek(wp, 2, 0) -> 0: 10 bytes on disk -- flushed first
   2 my_putc, my_fclose: "01XY456789"
   the same with naive_fseek: "\0\00123456789XY" (14 bytes)
   the digits were still in the buffer when lseek moved to 2, so all
   12 bytes went out at close, starting at 2 -- and the 2 bytes never
   written read back as zeros: a hole

7. my_fseek(fp, -10, 0) -> -1: a position before the start
   my_fseek(fp, 0, 7) -> -1: no origin 7
   still at 3, cnt 1021: next 3 bytes "孚" as if nothing happened
   my_fseek on it after my_fclose -> -1