shithub: femtolisp

ref: 1259c178373aa959a7bd0b7037b6ded11c46c47e
dir: /llt/attic/streams.c/

View raw version
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>
#include <limits.h>

#ifdef WIN32
#include <malloc.h>
#include <io.h>
#include <fcntl.h>
#include <errno.h>
#define fileno _fileno
#else
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#endif

#include "dtypes.h"
#include "utils.h"
#include "utf8.h"
#include "streams.h"

unsigned int type_sizes[] = {
    sizeof(int32_t), sizeof(int8_t), sizeof(int16_t),
    sizeof(int32_t), sizeof(float),  sizeof(double), sizeof(int64_t), 0,

    /* unsigned */
    0, sizeof(u_int8_t), sizeof(u_int16_t),
    sizeof(u_int32_t), 0, 0, sizeof(u_int64_t), 0,

    /* complex */
    2*sizeof(int32_t), 2*sizeof(int8_t), 2*sizeof(int16_t),
    2*sizeof(int32_t), 2*sizeof(float),  2*sizeof(double), 2*sizeof(int64_t),0,

    /* complex unsigned */
    0, 2*sizeof(u_int8_t), 2*sizeof(u_int16_t),
    2*sizeof(u_int32_t), 0, 0, 2*sizeof(u_int64_t), 0
};

bool_t valid_type(u_int32_t type)
{
    u_int32_t sz;

    /* irrelevant bits set */
    if (type & ~T_TYPEMASK)
        return false;
    sz = type & T_TYPESIZE;
    if (sz > T_INT64)
        return false;
    if (type == T_CPLX|T_BOOL)
        return false;
    /* no unsigned float or complex unsigned */
    if (type & T_UNSIGNED) {
        if ((sz > T_INT && sz != T_INT64) || sz == T_BOOL || type&T_CPLX)
            return false;
    }
    return true;
}

/* an important function: some stream routines need to call this at
   various points to ensure proper coexistence with bitwise I/O */
static void stream_bit_flush(stream_t *s)
{
    if (s->bitpos > 0) {
        stream_write(s, &s->bitbuf, 1);
        s->bitpos = 0;
    }
}

void stream_flush(stream_t *s)
{
    stream_bit_flush(s);
    s->funcs->flush(s);
}

void stream_free(stream_t *s)
{
    stream_flush(s);
    s->funcs->free_func(s);
}

int stream_readfd(stream_t *s)
{
    if (is_filestream(s)) {
        return fileno(s->fptr);
    }
    else if (is_pipestream(s)) {
        return s->rd;
    }
    return -1;
}

int stream_writefd(stream_t *s)
{
    if (is_filestream(s)) {
        return fileno(s->fptr);
    }
    else if (is_pipestream(s)) {
        return s->wd;
    }
    return -1;
}


/* file stream */

off_t fs_seek(struct _stream *s, off_t where)
{
    FILE *f = s->fptr;

    stream_bit_flush(s);

    if (fseek(f, where, SEEK_SET) == -1)
        return -1;
    s->pos = ftell(f);
    return s->pos;
}

off_t fs_skip(struct _stream *s, off_t offs)
{
    FILE *f = s->fptr;

    stream_bit_flush(s);

    // a successful fseek always resets the end-of-file condition, even if
    // the offset is zero. we change this so that moving by 0 bytes when you're
    // at eof means you stay at eof.
    if (offs == 0)
        return s->pos;

    if (fseek(f, offs, SEEK_CUR) == -1)
        return -1;
    s->pos += offs;
    return s->pos;
}

off_t fs_seek_end(struct _stream *s)
{
    FILE *f = s->fptr;

    stream_bit_flush(s);

    if (fseek(f, 0, SEEK_END) == -1)
        return -1;
    s->pos = ftell(f);
    return s->pos;
}

size_t fs_read(struct _stream *s, char *dest, size_t size)
{
    FILE *f = s->fptr;
    size_t c;

    c = fread(dest, 1, size, f); /* read single-byte entities */
    s->pos += c;
    return c;
}

size_t fs_write(struct _stream *s, char *data, size_t size)
{
    FILE *f = s->fptr;
    size_t c;

    c = fwrite(data, 1, size, f);
    s->pos += c;
    return c;
}

size_t fs_trunc(struct _stream *s, size_t size)
{
    FILE *f = s->fptr;

#ifndef WIN32
    if (ftruncate(fileno(f), s->pos) == -1)
        return -1; // TODO this should be unsigned!
#else
    if (_chsize(_fileno(f), s->pos) == -1)
        return -1;
#endif
    return s->pos;
}

void fs_flush(struct _stream *s)
{
    FILE *f = s->fptr;

    (void)fflush(f);
}

bool_t fs_eof(struct _stream *s)
{
    return (bool_t)feof(s->fptr);
}

void free_fs_stream(stream_t *s)
{
    fclose(s->fptr);
    free(s->name);
}

void stream_close(stream_t *s)
{
    /*
    if (!is_filestream(s))
        return;
    */
    stream_flush(s);
    // so far file streams are not designed to exist in the closed state
    //    fclose(s->fptr);
}

stream_interface_t fs_funcs =
    {free_fs_stream, fs_seek, fs_seek_end, fs_skip,
     fs_read, fs_write, fs_trunc, fs_eof, fs_flush};

stream_t *filestream_new(stream_t *s,
                         char *fName, bool_t create, bool_t rewrite)
{
    FILE *f;
    size_t sz;

    if (create) {
        if (rewrite) {
            f = fopen(fName, "w+b");
        }
        else {
            f = fopen(fName, "a+b");
            if (f)
                fseek(f, 0, SEEK_SET);
        }
    }
    else {
        f = fopen(fName, "r+b");
    }
    if (f == NULL) {
        /* try readonly */
        f = fopen(fName, "rb");
        if (f == NULL)
            return NULL;
    }

    s->fptr = f;
    s->name = strdup(fName);

    s->funcs = &fs_funcs;

    s->pos = 0;
    s->bitpos = s->bitbuf = 0;
    s->byteswap = false;
    return s;
}

stream_t *stream_fromfile(stream_t *s, FILE *f, char *name)
{
    s->fptr = f;
    s->name = strdup(name);

    s->funcs = &fs_funcs;

    s->pos = 0;
    s->bitpos = s->bitbuf = 0;
    s->byteswap = false;
    return s;
}

/* memory stream */

off_t ms_seek(struct _stream *s, off_t where)
{
    if ((size_t)where > s->size)
        return -1;

    stream_bit_flush(s);
    s->pos = where;

    return s->pos;
}

off_t ms_skip(struct _stream *s, off_t offs)
{
    if (s->pos+offs < 0 || s->pos+offs > s->size)
        return -1;

    stream_bit_flush(s);

    s->pos += offs;
    return s->pos;
}

off_t ms_seek_end(struct _stream *s)
{
    stream_bit_flush(s);
    s->pos = s->size;
    return s->pos;
}

size_t ms_read(struct _stream *s, char *dest, size_t size)
{
    size_t amt = size;

    assert(s->pos <= s->size);

    if (size == 0)
        return 0;

    if (s->size - s->pos < size)
        amt = s->size - s->pos;

    if (amt > 0) {
        memcpy(dest, &s->data[s->pos], amt);
    }

    s->pos += amt;
    return amt;
}

static char *ms_realloc(stream_t *s, size_t size)
{
    char *temp;

    if (size <= s->maxsize)
        return s->data;

    /* UNOFFICIALLY, we put a '\0' at the end of every memory stream for
       compatability with C library string functions, which are convenient.
       You are not allowed to depend on this behavior; it may eventually
       change.

       We implement this by telling everybody that maxsize is one less than
       the actual size of the buffer. They think the last byte is at index
       maxsize-1, but it is actually at index maxsize.

       data[s->size] and data[s->maxsize] are kept at '\0'. */

    if (size <= N_STREAM_LOCAL-1) {
        /* TO DO: if we want to allow shrinking, see if the buffer shrank
           down to this size, in which case we need to copy. */
        s->data = &s->local[0];
        s->maxsize = N_STREAM_LOCAL-1;
        s->data[s->maxsize] = '\0';
        return s->data;
    }
    if (s->data == &s->local[0]) {
        temp = malloc(size+1);
        // TODO nullcheck
        memcpy(temp, s->data, s->size);
    }
    else {
        /* here we rely on the realloc() behavior of
           doing a malloc() when passed a NULL pointer. */
        temp = realloc(s->data, size+1);
        if (temp == NULL)
            return NULL;
    }
    s->data = temp;
    s->maxsize = size;
    s->data[s->maxsize] = '\0';
    return s->data;
}

size_t ms_trunc(struct _stream *s, size_t size)
{
    if (size == s->size)
        return size;

    if (size < s->size) {
        // TODO: if big shrink, release space
        if (s->pos > size)
            s->pos = size;
    }
    else {
        if (ms_realloc(s, size) == NULL)
            return s->size;
    }

    s->size = size;
    s->data[size] = '\0';
    return size;
}

size_t ms_write(struct _stream *s, char *data, size_t size)
{
    size_t amt;
    size_t newsize;

    if (size == 0)
        return 0;

    if (s->pos + size > s->size) {
        if (s->pos + size > s->maxsize) {
            /* TO DO: here you might want to add a mechanism for limiting
               the growth of the stream. */
            newsize = s->maxsize * 2;
            while (s->pos + size > newsize)
                newsize *= 2;
            if (ms_realloc(s, newsize) == NULL) {
                /* no more space; write as much as we can */
                amt = s->maxsize - s->pos;
                if (amt > 0) {
                    memcpy(&s->data[s->pos], data, amt);
                }
                s->pos += amt;
                s->size = s->maxsize;
                /* in this case we've written up to the end of the buffer,
                   so we know the next char is \0 since ms_realloc sets that
                   up every time it allocates. */
                return amt;
            }
        }
        s->size = s->pos + size;

        /* always valid since secretly we know the buffer is 1 bigger than
           maxsize */
        s->data[s->size] = '\0';
    }
    memcpy(&s->data[s->pos], data, size);
    s->pos += size;

    return size;
}

void ms_flush(struct _stream *s)
{
}

bool_t ms_eof(struct _stream *s)
{
    assert(s->pos <= s->size);
    return (bool_t)(s->pos == s->size);
}

void free_ms_stream(stream_t *s)
{
    if (s->data != NULL && s->data != &s->local[0])
        free(s->data);
}
/*
stream_t *memstream_copy(stream_t *s)
{
    stream_t *ns = memstream_new(s->size);

    ms_write(ns, s->s, s->size);
    stream_seek(ns, 0);
    return ns;
}
*/
stream_interface_t ms_funcs =
    {free_ms_stream, ms_seek, ms_seek_end, ms_skip,
     ms_read, ms_write, ms_trunc, ms_eof, ms_flush};

stream_t *memstream_new(stream_t *s, size_t initsize)
{
    s->pos = 0;
    s->size = 0;
    s->data = &s->local[0];
    s->maxsize = N_STREAM_LOCAL-1;
    s->data[s->maxsize] = '\0';

    if (ms_realloc(s, initsize) == NULL)
        julia_outofmemory();

    s->data[initsize] = '\0';
    s->data[s->pos] = '\0';
    s->size = initsize;

    s->funcs = &ms_funcs;

    s->bitpos = s->bitbuf = 0;
    s->byteswap = false;

    return s;
}

#if 0
string_t *string_new(int len)
{
    string_t *str;

    str = memstream_new(len);
    str->len = len;

    return str;
}

/* this function makes string streams from C strings (NUL-term) */
string_t *string_fromc(char *s)
{
    string_t *str;
    int len = strlen(s);

    str = string_new(len);
    stream_write(str, s, len);
    stream_seek(str, 0);

    return str;
}
#endif

/* pipe */
off_t ps_seek(struct _stream *s, off_t where)
{
    return -1;
}

size_t ps_read(struct _stream *s, char *dest, size_t size);

off_t ps_skip(struct _stream *s, off_t offs)
{
    char buf[CHUNK_SIZE];
    int rd = s->rd;
    size_t c, amt;

    if (offs < 0)
        return -1;
    while (offs > 0) {
        amt = offs > CHUNK_SIZE ? CHUNK_SIZE : offs;
        c = ps_read(s, buf, amt);
        if (c < amt)
            return 0;
        offs -= c;
    }
    return 0;
}

off_t ps_seek_end(struct _stream *s)
{
    return -1;
}

size_t ps_read(struct _stream *s, char *dest, size_t size)
{
    if (ps_eof(s))
        return 0;
#ifdef WIN32
    int c = _read(s->rd, dest, size);
#else
    ssize_t c = read(s->rd, dest, size);
#endif
    if (c < 0)
        return 0;
    return (size_t)c;
}

size_t ps_write(struct _stream *s, char *data, size_t size)
{
#ifdef WIN32
    int c = _write(s->wd, data, size);
#else
    ssize_t c = write(s->wd, data, size);
#endif
    if (c < 0)
        return 0;
    return c;
}

size_t ps_trunc(struct _stream *s, size_t size)
{
    return 0;
}

void ps_flush(struct _stream *s)
{
}

bool_t ps_eof(struct _stream *s)
{
#ifndef WIN32
    fd_set set;
    struct timeval tv = {0, 0};

    FD_ZERO(&set);
    FD_SET(s->rd, &set);
    return (select(s->rd+1, &set, NULL, NULL, &tv)==0);
#else
    return 0;
#endif
}

void free_ps_stream(stream_t *s)
{
    close(s->rd);
    close(s->wd);
}

stream_interface_t ps_funcs =
    {free_ps_stream, ps_seek, ps_seek_end, ps_skip,
     ps_read, ps_write, ps_trunc, ps_eof, ps_flush};

stream_t *pipestream_new(stream_t *s, int flags)
{
    int fds[2];

    s->funcs = &ps_funcs;

#ifdef WIN32
    _pipe(&fds[0], 32768, _O_BINARY | flags);
#else
    pipe(&fds[0]);
#endif
    s->rd = fds[0]; s->wd = fds[1];

    s->byteswap = false;
    s->bitpos = s->bitbuf = 0;
    s->pos = 0;

    return s;
}


/* high-level stream functions */

/* type is a "T_" code as defined in streams.h */
int stream_put_num(stream_t *stream, char *d, u_int32_t type)
{
    int c, sz, i;

    assert(valid_type(type));
    sz = type_sizes[type];

    if (!stream->byteswap) {
        c = stream_write(stream, d, sz);
    }
    else if (sz >= 4) {
        char temp[32];
        if (type & T_CPLX) {
            bswap_to(temp,      (byte_t*)d,      sz/2);
            bswap_to(temp+sz/2, (byte_t*)d+sz/2, sz/2);
        }
        else {
            bswap_to(temp, (byte_t*)d, sz);
        }
        c = stream_write(stream, temp, sz);
    }
    else {
        assert(sz == 2 || sz == 1);
        if (type & T_CPLX) {
            c = stream_write(stream, &d[0], 2);
            /*
            for(i=sz/2-1; i >= 0; i--) {
                c += stream_write(stream, &d[i], 1);
            }
            for(i=sz-1; i >= sz/2; i--) {
                c += stream_write(stream, &d[i], 1);
            }
            */
        }
        else {
            c = 0;
            if (sz == 2)
                c += stream_write(stream, &d[1], 1);
            c += stream_write(stream, &d[0], 1);
        }
    }

    return c;
}

int stream_get_num(stream_t *s, char *data, u_int32_t type)
{
    int c, sz;

    assert(valid_type(type));
    sz = type_sizes[type];

    c = stream_read(s, data, sz);

    if (s->byteswap && c == sz) {
        if (type & T_CPLX) {
            bswap((byte_t*)data, sz/2);
            bswap((byte_t*)data+sz/2, sz/2);
        }
        else {
            bswap((byte_t*)data, sz);
        }
    }
    if (c < sz)
        return -1;
    return c;
}

int stream_put_int(stream_t *s, int n)
{
    return stream_put_num(s, (char*)&n, T_INT);
}

int stream_put_char(stream_t *s, u_int32_t wc)
{
    char buf[8];
    int amt;

    amt = u8_wc_toutf8(buf, wc);
    return stream_write(s, buf, amt);
}

int stream_put_stringz(stream_t *s, char *str, bool_t nulterm)
{
    int c, l = strlen(str);

    c = stream_write(s, str, l);
    if (nulterm)
        c += stream_write(s, &str[l], 1);
    return c;
}

int stream_get_char(stream_t *s, u_int32_t *pwc)
{
    char buf[8];
    int amt;
    unsigned int i;

    amt = stream_read(s, buf, 1);
    if (amt == 0)
        return 0;
    amt = u8_seqlen(buf) - 1;  // find out how many more bytes in this seq
    if (amt) {
        stream_read(s, &buf[1], amt);
    }
    amt++;
    buf[amt] = '\0';
    i=0;
    *pwc = u8_nextmemchar(buf, &i);
    return i;
}

int stream_nextchar(stream_t *s)
{
    u_int32_t wc;

    if (is_memstream(s)) {
        if (stream_eof(s))
            return -1;
        stream_bit_flush(s);
        s->pos++;
        while (!stream_eof(s) && !isutf(s->s[s->pos]))
            s->pos++;
        return s->pos;
    }

    if (stream_get_char(s, &wc) == 0)
        return -1;
    return s->pos;
}

int stream_prevchar(stream_t *s)
{
    char c;

    if (is_memstream(s)) {
        if (s->pos == 0)
            return -1;
        stream_bit_flush(s);
        s->pos--;
        while (s->pos > 0 && !isutf(s->s[s->pos]))
            s->pos--;
        return s->pos;
    }

    do {
        if (stream_skip(s, -1) == -1)
            return -1;
        stream_read(s, &c, 1);
        stream_skip(s, -1);
    } while(!isutf(c));
    return s->pos;
}

void stream_put_bit(stream_t *s, int bit)
{
    byte_t mask = 0x1;

    if (s->bitpos == 0) {
        if (!stream_read(s, &s->bitbuf, 1)) {
            s->bitbuf = 0;
        }
        else {
            stream_skip(s, -1);
        }
    }

    mask <<= s->bitpos;
    if (bit)
        s->bitbuf |= mask;
    else
        s->bitbuf &= ~mask;
    s->dirty = 1;

    s->bitpos++;
    if (s->bitpos > 7) {
        s->bitpos = 0;
        stream_write(s, &s->bitbuf, 1);
    }
}

int stream_get_bit(stream_t *s, int *pbit)
{
    byte_t mask = 0x1;

    if (s->bitpos == 0) {
        if (!stream_read(s, &s->bitbuf, 1)) {
            return 0;
        }
        else {
            stream_skip(s, -1);
        }
        s->dirty = 0;
    }

    mask <<= s->bitpos;
    *pbit = (s->bitbuf & mask) ? 1 : 0;

    s->bitpos++;

    if (s->bitpos > 7) {
        s->bitpos = 0;
        if (s->dirty) {
            stream_write(s, &s->bitbuf, 1);
        }
        else {
            stream_skip(s, 1);
        }
    }
    return 1;
}

/* warning: DO NOT write a trivial wrapper for this function; it allows
   easily crashing the interpreter using unfriendly format strings.

   also, this function is designed to print small things like messages. it
   cannot print arbitrarily large data. to do that, use stream_write,
   or repeated calls to this function.

   TODO: this doesn't handle UTF-8 properly:

printf("%*s|\n%*s|\n", -4, "X", -4, "a")
printf("%-4s|\n%-4s|\n", "X", "a")
X  |
a   |

Where X is a 2-byte character.
*/
int stream_printf(stream_t *s, char *format, ...)
{
    int c;
    va_list args;
    char buf[512];

    va_start(args, format);
    c = vsnprintf(buf, sizeof(buf), format, args);
    va_end(args);

    if (c < 0)
        return 0;

    if ((unsigned)c > sizeof(buf))
        c = sizeof(buf);

    return stream_write(s, buf, c);
}

char *stream_take_buffer(stream_t *s, size_t *size)
{
    char *buf;

    if (!is_memstream(s) || s->data == NULL)
        return NULL;

    stream_flush(s);

    if (s->data == &s->local[0]) {
        buf = malloc(s->size+1);
        if (buf == NULL)
            return NULL;
        memcpy(buf, s->data, s->size+1);
        buf[s->size] = '\0';
    }
    else {
        buf = s->data;
    }

    *size = s->size+1; /* buffer is actually 1 bigger for terminating NUL */

    /* empty stream and reinitialize */
    s->data = &s->local[0];
    s->maxsize = N_STREAM_LOCAL-1;
    s->data[s->maxsize] = '\0';
    stream_trunc(s, 0);

    return buf;
}

/* Chunk size for reading lines: if too small, then we make too many low-level
   calls. if too large, then we waste time reading bytes beyond the end of the
   current line. this is effectively a guess as to how big a line is.
   this value should be < N_STREAM_LOCAL, allowing at least some lines to
   fit without extra allocation. */
#define LINE_CHUNK_SIZE (N_STREAM_LOCAL-1)

int stream_readline(stream_t *dest, stream_t *s, char delim)
{
    char chunk[LINE_CHUNK_SIZE];
    int i, cnt, total=0;

    if (stream_eof(s))
        return 0;

    do {
        cnt = stream_read(s, chunk, LINE_CHUNK_SIZE);
        for(i=0; i < cnt; i++) {
            if (chunk[i] == delim)
                break;
        }
        if (i < cnt) {
            total += stream_write(dest, chunk, i+1);
            stream_skip(s, -(cnt-(i+1)));
            break;
        }
        total += stream_write(dest, chunk, cnt);
    } while (!stream_eof(s));

    return total;
}

/* extract one line of text from a stream, into a memory buffer.
   a pointer to the buffer is returned in *pbuf, the size of the buffer
   in *psz, and the number of characters in the line (including newline) is
   returned. the line may contain NULs, but there will also be a NUL
   terminator as the last character in the buffer.

   This routine's behavior is not exactly the same as GNU getline. In
   particular, it always allocates a buffer.
*/
int stream_getline(stream_t *s, char **pbuf, size_t *psz)
{
    stream_t buf;

    memstream_new(&buf, 0);

    stream_readline(&buf, s, '\n');

    *pbuf = stream_take_buffer(&buf, psz);

    return *psz-1;
}

int stream_get_stringz(stream_t *dest, stream_t *src)
{
    return stream_readline(dest, src, '\0');
}

/* get n UTF-8 characters */
int stream_get_stringn(stream_t *dest, stream_t *src, size_t c)
{
    u_int32_t wc;
    size_t cnt=0;

    while (c > 0) {
        if (stream_get_char(src, &wc) == 0 ||
            stream_put_char(dest, wc) == 0)
            break;
        c--;
        cnt++;
    }
    return cnt;
}

/* TODO: change API to allow passing a heap-allocated page-aligned
   chunk to reuse on each copy. if it's NULL we alloc our own. */
int stream_copy(stream_t *to, stream_t *from, size_t nbytes, bool_t all)
{
    int c=0, cnt, wc, rdc;
    char buf[CHUNK_SIZE];
    int remain = all ? CHUNK_SIZE : nbytes;

    if (is_memstream(to)) {
        // avoid extra copy, read directly into memstream
        while (!stream_eof(from)) {
            if (all) {
                rdc = CHUNK_SIZE;
            }
            else {
                /* if !all, only 1 call to stream_read needed */
                rdc = nbytes;
            }

            if (ms_realloc(to, to->pos + rdc) == NULL) {
                rdc = to->maxsize - to->pos;
                if (rdc == 0)
                    break;
            }
            cnt = stream_read(from, &to->s[to->pos], rdc);
            wc = cnt;
            to->pos += wc;
            if (to->pos > to->size) {
                to->size = to->pos;
                to->s[to->size] = '\0';
            }
            c += wc;

            if (!all)
                remain -= wc;
            if (wc < rdc || remain == 0)
                break;
        }
    }
    else if (is_memstream(from)) {
        while (1) {
            if (all) {
                rdc = CHUNK_SIZE;
            }
            else {
                /* if !all, only 1 call to stream_read needed */
                rdc = nbytes;
            }
            // check for source out of data
            if (from->size - from->pos < rdc) {
                remain = rdc = from->size - from->pos;
            }
            cnt = stream_write(to, &from->s[from->pos], rdc);
            wc = cnt;
            from->pos += wc;
            c += wc;
            if (!all)
                remain -= wc;
            if (wc < rdc || remain == 0)
                break;
        }
    }
    else {
        while (!stream_eof(from)) {
            rdc = remain>CHUNK_SIZE ? CHUNK_SIZE : remain;

            cnt = stream_read(from, buf, rdc);
            wc = stream_write(to, buf, cnt);
            c += wc;

            if (!all)
                remain -= wc;
            if (wc < rdc || remain == 0)
                break;
        }
    }
    return c;
}


/* serialization functions */

/* nbytes is either 4 or 8 */
size_t stream_get_offset(stream_t *s, off_t *po, int nbytes)
{
    size_t c;
    int64_t off64;
    int32_t off32;

    if (nbytes == 4) {
        c = stream_read(s, (char*)&off32, nbytes);
        if (c < nbytes)
            return c;
        if (s->byteswap)
            off32 = bswap_32(off32);
        /* OK on either system since off_t is >= off32 in size */
        *po = (off_t)off32;
    }
    else {
        c = stream_read(s, (char*)&off64, nbytes);
        if (c < nbytes)
            return c;
        if (s->byteswap)
            off64 = bswap_64(off64);
        if (sizeof(off_t) == 8) {
            *po = (off_t)off64;
        }
        else {
            if (off64 <= INT_MAX && off64 >= INT_MIN) {
                // downcast safe
                *po = (off_t)off64;
            }
            else {
                cerror("I/O error: 64-bit offset truncated on input.\n");
            }
        }
    }

    return c;
}

size_t stream_put_offset(stream_t *s, off_t o, int nbytes)
{
    int64_t off64;
    int32_t off32;

    if (nbytes == 4) {
        if (sizeof(off_t) == 8 && (o > INT_MAX || o < INT_MIN)) {
            cerror("I/O error: 64-bit offset truncated on output.\n");
        }
        off32 = (int32_t)o;
        if (s->byteswap)
            off32 = bswap_32(off32);
        return stream_write(s, (char*)&off32, 4);
    }
    off64 = (int64_t)o;
    if (s->byteswap)
        off64 = bswap_64(off64);
    return stream_write(s, (char*)&off64, 8);
}