K&R C Exercise 8-4: Write fseek to Coordinate with Buffering

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.

lseek moves the kernel’s idea of the file position. But our mini stdio keeps its own position — inside the buffer — and the two disagree almost all the time. After you getc five characters of a 36-byte file, the kernel is already at offset 36 (one read pulled the whole file into the buffer) while the reader is logically at offset 5. A naive fseek that just calls lseek would be wrong in both directions:

  • Reading, SEEK_CUR: “current” to the caller means the buffer position, not the kernel’s. The fix is one elegant line: subtract the unread byte count — offset -= fp->cnt — then discard the buffer.
  • Writing: buffered bytes not yet written would land at the new offset after the seek. So flush first, then seek.

As a bonus, seeking must clear the EOF flag — after reading to the end, a seek back makes the stream readable again, exactly like real fseek.

Solution

/* K&R Exercise 8-4: fseek for the mini stdio — must coordinate with
 * the buffering done by _fillbuf and _flushbuf.
 * Compile: gcc -ansi -Wall -Wextra ex8_4.c -o ex8_4 */
#define _POSIX_C_SOURCE 200112L
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>          /* printf for the demo only */

#define PERMS       0666
#define MY_OPEN_MAX 20
#define MYBUFSIZ    1024
#define MY_EOF      (-1)

/* origin values, same meanings as SEEK_SET / SEEK_CUR / SEEK_END */
#define MY_SEEK_SET 0
#define MY_SEEK_CUR 1
#define MY_SEEK_END 2

typedef struct {
    int   cnt;
    char *ptr;
    char *base;
    struct {
        unsigned is_read  : 1;
        unsigned is_write : 1;
        unsigned is_unbuf : 1;
        unsigned is_eof   : 1;
        unsigned is_err   : 1;
    } flag;
    int   fd;
} MYFILE;

static MYFILE _iob[MY_OPEN_MAX];

#define my_getc(p)    (--(p)->cnt >= 0 \
    ? (unsigned char) *(p)->ptr++ : _fillbuf(p))
#define my_putc(x, p) (--(p)->cnt >= 0 \
    ? (unsigned char)(*(p)->ptr++ = (char)(x)) : _flushbuf((x), p))

static int _fillbuf(MYFILE *fp)
{
    int bufsize;
    ssize_t n;

    if (!fp->flag.is_read || fp->flag.is_eof || fp->flag.is_err)
        return MY_EOF;
    bufsize = fp->flag.is_unbuf ? 1 : MYBUFSIZ;
    if (fp->base == NULL)
        if ((fp->base = malloc((size_t)bufsize)) == NULL)
            return MY_EOF;
    fp->ptr = fp->base;
    n = read(fp->fd, fp->ptr, (size_t)bufsize);
    fp->cnt = (int)n;
    if (--fp->cnt < 0) {
        if (fp->cnt == -1)
            fp->flag.is_eof = 1;
        else
            fp->flag.is_err = 1;
        fp->cnt = 0;
        return MY_EOF;
    }
    return (unsigned char) *fp->ptr++;
}

static int _flushbuf(int c, MYFILE *fp)
{
    int bufsize;
    ssize_t nc;

    if (!fp->flag.is_write || fp->flag.is_err)
        return MY_EOF;
    bufsize = fp->flag.is_unbuf ? 1 : MYBUFSIZ;
    if (fp->base == NULL) {
        if ((fp->base = malloc((size_t)bufsize)) == NULL) {
            fp->flag.is_err = 1;
            return MY_EOF;
        }
    } else {
        nc = fp->ptr - fp->base;
        if (nc > 0 && write(fp->fd, fp->base, (size_t)nc) != nc) {
            fp->flag.is_err = 1;
            return MY_EOF;
        }
    }
    fp->ptr = fp->base;
    if (c == MY_EOF) {
        fp->cnt = bufsize;
        return 0;
    }
    fp->cnt = bufsize - 1;
    *fp->ptr++ = (char)c;
    return (unsigned char)c;
}

static int my_fflush(MYFILE *fp)
{
    if (fp == NULL || !fp->flag.is_write)
        return MY_EOF;
    if (fp->base == NULL)
        return 0;
    return _flushbuf(MY_EOF, fp) == MY_EOF ? MY_EOF : 0;
}

/* my_fseek: THE exercise. Two jobs beyond calling lseek:
 *  1. reading — the buffer may hold bytes the caller has not seen, so a
 *     relative seek must subtract cnt, and the buffer must be discarded;
 *  2. writing — pending buffered bytes must be flushed first, or they
 *     would land at the NEW offset. */
static int my_fseek(MYFILE *fp, long offset, int origin)
{
    long rc = 0;

    if (fp == NULL || fp->flag.is_err)
        return -1;
    if (fp->flag.is_read) {
        if (origin == MY_SEEK_CUR)
            offset -= fp->cnt;          /* unread buffered bytes */
        rc = lseek(fp->fd, offset, origin);
        fp->cnt = 0;                    /* discard the buffer */
        fp->ptr = fp->base;
        fp->flag.is_eof = 0;            /* seeking clears end-of-file */
    } else if (fp->flag.is_write) {
        if (my_fflush(fp) == MY_EOF)
            return -1;
        rc = lseek(fp->fd, offset, origin);
    }
    return rc == -1 ? -1 : 0;
}

/* my_fclose: from Exercise 8-3 — flush, free the buffer, close */
static int my_fclose(MYFILE *fp)
{
    int rc;

    if (fp == NULL)
        return MY_EOF;
    rc = 0;
    if (fp->flag.is_write && my_fflush(fp) == MY_EOF)
        rc = MY_EOF;
    free(fp->base);
    fp->base = fp->ptr = NULL;
    fp->cnt = 0;
    fp->flag.is_read = fp->flag.is_write = 0;
    fp->flag.is_unbuf = fp->flag.is_eof = fp->flag.is_err = 0;
    if (close(fp->fd) == -1)
        rc = MY_EOF;
    return rc;
}

static MYFILE *my_fopen(const char *name, char mode)
{
    int fd;
    MYFILE *fp;

    if (mode != 'r' && mode != 'w' && mode != 'a')
        return NULL;
    for (fp = _iob; fp < _iob + MY_OPEN_MAX; fp++)
        if (!fp->flag.is_read && !fp->flag.is_write)
            break;
    if (fp >= _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.is_unbuf = fp->flag.is_eof = fp->flag.is_err = 0;
    if (mode == 'r') {
        fp->flag.is_read = 1;
        fp->flag.is_write = 0;
    } else {
        fp->flag.is_read = 0;
        fp->flag.is_write = 1;
    }
    return fp;
}

int main(void)
{
    MYFILE *fp;
    int c, i;

    /* build a 36-byte test file through OUR write layer,
     * using my_fseek on the write side to overwrite byte 10 */
    if ((fp = my_fopen("ex8_4_test.txt", 'w')) == NULL)
        return 1;
    for (c = 'A'; c <= 'Z'; c++)
        my_putc(c, fp);                 /* ABC...Z, all still buffered */
    for (c = '0'; c <= '9'; c++)
        my_putc(c, fp);                 /* ...then 0123456789 */
    /* overwrite position 10 ('K') with '#': fseek MUST flush first */
    if (my_fseek(fp, 10L, MY_SEEK_SET) == -1)
        return 1;
    my_putc('#', fp);
    my_fclose(fp);                      /* final flush + free + close */

    /* now read it back with buffered reads + seeks */
    if ((fp = my_fopen("ex8_4_test.txt", 'r')) == NULL)
        return 1;
    printf("first 5 chars:  ");
    for (i = 0; i < 5; i++)
        putchar(my_getc(fp));           /* buffer now holds ALL 36 bytes */
    putchar('\n');

    /* relative seek: +5 from position 5 must land on byte 10 ('#'),
     * even though 31 unread bytes sit in the buffer */
    my_fseek(fp, 5L, MY_SEEK_CUR);
    printf("SEEK_CUR +5:    %c  (expect #)\n", my_getc(fp));

    my_fseek(fp, 0L, MY_SEEK_SET);
    printf("SEEK_SET 0:     %c  (expect A)\n", my_getc(fp));

    my_fseek(fp, -1L, MY_SEEK_END);
    printf("SEEK_END -1:    %c  (expect 9)\n", my_getc(fp));

    printf("getc after EOF-clearing seek works: ");
    while ((c = my_getc(fp)) != MY_EOF)  /* drains to EOF */
        ;
    my_fseek(fp, 25L, MY_SEEK_SET);
    printf("%c  (expect Z)\n", my_getc(fp));
    return 0;
}

Compile and Run

gcc -ansi -Wall -Wextra ex8_4.c -o ex8_4
./ex8_4

Sample Output

first 5 chars:  ABCDE
SEEK_CUR +5:    #  (expect #)
SEEK_SET 0:     A  (expect A)
SEEK_END -1:    9  (expect 9)
getc after EOF-clearing seek works: Z  (expect Z)

Real captured run. The SEEK_CUR line is the whole exercise in one character: the kernel sat at offset 36, yet “+5 from here” correctly resolved against the reader’s logical position 5 — because my_fseek subtracted the 31 unread buffered bytes. And byte 10 reads back as #, proving the write-side seek flushed ABC...Z0123456789 before moving the offset.

What This Exercise Teaches

  • The two file positions: the kernel’s (moved by read/lseek) vs. the buffered library’s logical one
  • The offset -= fp->cnt correction — the heart of real stdio’s fseek
  • Why seeking on a write stream must flush first, or bytes land at the wrong offset
  • Seek clears EOF: how fseek revives a stream that has returned EOF

Set Up Your C Environment

This chapter uses UNIX system calls — a Linux machine, macOS terminal, or WSL2 on Windows all work:

← Exercise 8-3  | 
All K&R Solutions

Book:

The C Programming Language, 2nd Ed — Kernighan & Ritchie

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>