7 n__zip_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

src/n_zip.c

Reading members out of a ZIP archive, to a file. More...

Structures

Name
struct ZIP_SINK
where unpacked bytes go: a file, or a buffer in memory

Functions

Name
int zip_unpack(const char * path, const char * name, const N_ZIP_MEMBER * member, ZIP_SINK * sink)
unpack a member into a sink, checking its size and CRC-32
int zip_scan(const char * path, const char * name, int ending, N_ZIP_MEMBER * member, char * found_name, size_t found_len)
find a member by its whole name, or by how its name ends
int sink_put(ZIP_SINK * sink, const unsigned char * data, size_t len)
put unpacked bytes where they go
int seek_to(FILE * f, uint64_t at)
helper: seek to a 64 bit offset where the platform's fseek takes a long
uint32_t le32(const unsigned char * p)
helper: a little endian 32 bit value
unsigned le16(const unsigned char * p)
helper: a little endian 16 bit value

Defines

Name
N_ZIP_TAIL
how much of an archive's end is searched for its end record: the record and the longest comment it may carry
N_ZIP_CHUNK
the pieces a member is read and written in

Detailed Description

Reading members out of a ZIP archive, to a file.

Author: Castagnier Mickael

Version: 1.0

Date: 28/09/2026

Functions Documentation

function zip_unpack

static int zip_unpack(
    const char * path,
    const char * name,
    const N_ZIP_MEMBER * member,
    ZIP_SINK * sink
)

unpack a member into a sink, checking its size and CRC-32

Parameters:

  • path the archive
  • name the member
  • member what n_zip_find() said of it
  • sink where the bytes go

Return: 0, or -1

function zip_scan

static int zip_scan(
    const char * path,
    const char * name,
    int ending,
    N_ZIP_MEMBER * member,
    char * found_name,
    size_t found_len
)

find a member by its whole name, or by how its name ends

Parameters:

  • path the archive
  • name the name, or its ending
  • ending non-zero to match the ending only
  • member receives what it is, may be NULL
  • found_name receives the whole name when matching an ending, may be NULL
  • found_len size of found_name

Return: 0 when found, -1 otherwise

function sink_put

static int sink_put(
    ZIP_SINK * sink,
    const unsigned char * data,
    size_t len
)

put unpacked bytes where they go

Parameters:

  • sink the destination
  • data the bytes
  • len how many

Return: 0, or -1 when the file refused them or the buffer is full

function seek_to

static int seek_to(
    FILE * f,
    uint64_t at
)

helper: seek to a 64 bit offset where the platform's fseek takes a long

function le32

static uint32_t le32(
    const unsigned char * p
)

helper: a little endian 32 bit value

function le16

static unsigned le16(
    const unsigned char * p
)

helper: a little endian 16 bit value

Macros Documentation

define N_ZIP_TAIL

#define N_ZIP_TAIL (22 + 65535)

how much of an archive's end is searched for its end record: the record and the longest comment it may carry

define N_ZIP_CHUNK

#define N_ZIP_CHUNK 262144

the pieces a member is read and written in

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

#include <zlib.h>

#include "nilorea/n_log.h"
#include "nilorea/n_zip.h"

#define N_ZIP_TAIL (22 + 65535)
#define N_ZIP_CHUNK 262144

static unsigned le16(const unsigned char* p) {
    return (unsigned)p[0] | ((unsigned)p[1] << 8);
}
static uint32_t le32(const unsigned char* p) {
    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

static int seek_to(FILE* f, uint64_t at) {
    if (at > (uint64_t)0x7fffffff) return -1;
    return fseek(f, (long)at, SEEK_SET);
}

static int zip_scan(const char* path, const char* name, int ending, N_ZIP_MEMBER* member, char* found_name, size_t found_len) {
    FILE* f = NULL;
    unsigned char* tail = NULL;
    long size = 0, start = 0, i = 0;
    size_t got = 0, name_len = 0;
    uint32_t dir_size = 0, dir_at = 0;
    unsigned entries = 0, k = 0;
    int found = -1;

    if (!path || !name) return -1;
    name_len = strlen(name);
    f = fopen(path, "rb");
    if (!f) return -1;
    if (fseek(f, 0, SEEK_END) != 0 || (size = ftell(f)) < 22) {
        fclose(f);
        return -1;
    }
    start = size > N_ZIP_TAIL ? size - N_ZIP_TAIL : 0;
    tail = (unsigned char*)malloc((size_t)(size - start));
    if (!tail || fseek(f, start, SEEK_SET) != 0 || (got = fread(tail, 1, (size_t)(size - start), f)) < 22) {
        free(tail);
        fclose(f);
        return -1;
    }
    /* The end of central directory record, the last one in the file. */
    for (i = (long)got - 22; i >= 0; i--) {
        if (le32(tail + i) == 0x06054b50u) break;
    }
    if (i < 0) {
        free(tail);
        fclose(f);
        return -1;
    }
    entries = le16(tail + i + 10);
    dir_size = le32(tail + i + 12);
    dir_at = le32(tail + i + 16);
    free(tail);
    tail = NULL;
    if (dir_at == 0xffffffffu || dir_size == 0xffffffffu || (long)dir_at + (long)dir_size > size) {
        n_log(LOG_ERR, "zip: %s is ZIP64 or damaged, not read", path);
        fclose(f);
        return -1;
    }
    tail = (unsigned char*)malloc(dir_size ? dir_size : 1);
    if (!tail || seek_to(f, dir_at) != 0 || fread(tail, 1, dir_size, f) != dir_size) {
        free(tail);
        fclose(f);
        return -1;
    }
    fclose(f);
    /* The central directory: one record a member, each 46 bytes and then
     * its name, extra field and comment. */
    for (k = 0, i = 0; k < entries && (uint32_t)i + 46 <= dir_size; k++) {
        const unsigned char* r = tail + i;
        unsigned n = 0, extra = 0, comment = 0;

        if (le32(r) != 0x02014b50u) break;
        n = le16(r + 28);
        extra = le16(r + 30);
        comment = le16(r + 32);
        if ((uint32_t)i + 46 + n > dir_size) break;
        if ((!ending && n == name_len && memcmp(r + 46, name, n) == 0) ||
            (ending && n >= name_len && memcmp(r + 46 + n - name_len, name, name_len) == 0)) {
            if (found_name && found_len) {
                size_t copy = n < found_len - 1 ? n : found_len - 1;

                memcpy(found_name, r + 46, copy);
                found_name[copy] = '\0';
            }
            if (member) {
                member->method = le16(r + 10);
                member->crc = le32(r + 16);
                member->compressed = le32(r + 20);
                member->size = le32(r + 24);
                member->offset = le32(r + 42);
            }
            found = (le32(r + 20) == 0xffffffffu || le32(r + 24) == 0xffffffffu) ? -1 : 0;
            break;
        }
        i += 46 + (long)n + (long)extra + (long)comment;
    }
    free(tail);
    return found;
}

int n_zip_find(const char* path, const char* name, N_ZIP_MEMBER* member) {
    return zip_scan(path, name, 0, member, NULL, 0);
}

int n_zip_find_ending(const char* path, const char* ending, char* name, size_t name_len) {
    if (!name || !name_len) return -1;
    return zip_scan(path, ending, 1, NULL, name, name_len);
}

typedef struct {
    FILE* file;
    unsigned char* buffer;
    size_t used;
    size_t size;
} ZIP_SINK;

static int sink_put(ZIP_SINK* sink, const unsigned char* data, size_t len) {
    if (sink->file) return fwrite(data, 1, len, sink->file) == len ? 0 : -1;
    if (len > sink->size - sink->used) return -1;
    memcpy(sink->buffer + sink->used, data, len);
    sink->used += len;
    return 0;
}

static int zip_unpack(const char* path, const char* name, const N_ZIP_MEMBER* member, ZIP_SINK* sink) {
    unsigned char local[30];
    unsigned char *in_buf = NULL, *out_buf = NULL;
    FILE* f = NULL;
    uint64_t left = 0, written = 0;
    uLong crc = crc32(0L, Z_NULL, 0);
    z_stream zs;
    int zs_ready = 0, result = -1;

    if (member->method != 0 && member->method != 8) {
        n_log(LOG_ERR, "zip: %s in %s is compressed by method %u, not read", name, path, member->method);
        return -1;
    }
    f = fopen(path, "rb");
    if (!f) return -1;
    /* The local header's own name and extra lengths, which may differ from
     * the central directory's, say where the data begins. */
    if (seek_to(f, member->offset) != 0 || fread(local, 1, 30, f) != 30 || le32(local) != 0x04034b50u ||
        seek_to(f, member->offset + 30 + le16(local + 26) + le16(local + 28)) != 0) {
        fclose(f);
        return -1;
    }
    in_buf = (unsigned char*)malloc(N_ZIP_CHUNK);
    out_buf = (unsigned char*)malloc(N_ZIP_CHUNK);
    if (!in_buf || !out_buf) goto done;
    memset(&zs, 0, sizeof(zs));
    /* Raw deflate: a ZIP member has no zlib header. */
    if (member->method == 8) {
        if (inflateInit2(&zs, -MAX_WBITS) != Z_OK) goto done;
        zs_ready = 1;
    }
    left = member->compressed;
    while (left > 0) {
        size_t take = left < N_ZIP_CHUNK ? (size_t)left : N_ZIP_CHUNK;

        if (fread(in_buf, 1, take, f) != take) goto done;
        left -= take;
        if (member->method == 0) {
            if (sink_put(sink, in_buf, take) != 0) goto done;
            crc = crc32(crc, in_buf, (uInt)take);
            written += take;
            continue;
        }
        zs.next_in = in_buf;
        zs.avail_in = (uInt)take;
        do {
            int rc = 0;
            size_t have = 0;

            zs.next_out = out_buf;
            zs.avail_out = N_ZIP_CHUNK;
            rc = inflate(&zs, Z_NO_FLUSH);
            if (rc != Z_OK && rc != Z_STREAM_END && rc != Z_BUF_ERROR) goto done;
            have = N_ZIP_CHUNK - zs.avail_out;
            if (have && sink_put(sink, out_buf, have) != 0) goto done;
            crc = crc32(crc, out_buf, (uInt)have);
            written += have;
            if (rc == Z_STREAM_END) break;
        } while (zs.avail_in > 0 || zs.avail_out == 0);
    }
    if (written != member->size || (uint32_t)crc != member->crc) {
        n_log(LOG_ERR, "zip: %s in %s came out %llu bytes, crc %08lx; %llu, %08x expected", name, path,
              (unsigned long long)written, (unsigned long)crc, (unsigned long long)member->size, member->crc);
        goto done;
    }
    result = 0;

done:
    if (zs_ready) inflateEnd(&zs);
    free(in_buf);
    free(out_buf);
    fclose(f);
    return result;
}

int n_zip_extract(const char* path, const char* name, const char* out) {
    N_ZIP_MEMBER member;
    ZIP_SINK sink;
    int result = -1;

    if (!out || n_zip_find(path, name, &member) != 0) return -1;
    memset(&sink, 0, sizeof(sink));
    sink.file = fopen(out, "wb");
    if (!sink.file) return -1;
    result = zip_unpack(path, name, &member, &sink);
    if (fclose(sink.file) != 0) result = -1;
    if (result != 0) remove(out);
    return result;
}

int n_zip_read(const char* path, const char* name, size_t max_size, unsigned char** data, size_t* len) {
    N_ZIP_MEMBER member;
    ZIP_SINK sink;

    if (!data || !len) return -1;
    *data = NULL;
    *len = 0;
    if (n_zip_find(path, name, &member) != 0) return -1;
    if (member.size > max_size || member.size > (uint64_t)SIZE_MAX - 1) {
        n_log(LOG_ERR, "zip: %s in %s is %llu bytes, more than the %zu allowed", name, path,
              (unsigned long long)member.size, max_size);
        return -1;
    }
    memset(&sink, 0, sizeof(sink));
    sink.size = (size_t)member.size;
    /* One more byte, so a text member can be ended with a zero. */
    sink.buffer = (unsigned char*)malloc(sink.size + 1);
    if (!sink.buffer) return -1;
    if (zip_unpack(path, name, &member, &sink) != 0 || sink.used != sink.size) {
        free(sink.buffer);
        return -1;
    }
    sink.buffer[sink.size] = 0;
    *data = sink.buffer;
    *len = sink.size;
    return 0;
}