/*
* Copyright (C) 2023 Patrick McDermott
*
* This file is part of opkg-opk.
*
* opkg-opk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opkg-opk is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with opkg-opk. If not, see .
*/
#include
#include
#include
#include
#include "defs.h"
#include "dirent.h"
#include "gzip.h"
#include "ustar.h"
#define OPKG_OPK_USTAR_NUM_BASE_ 8
/* #define OPKG_OPK_USTAR_BB_EXACT_HEADER_ 1 */
struct _opkg_opk_ustar_header {
char name [100];
char mode [8];
char uid [8];
char gid [8];
char size [12];
char mtime [12];
char chksum [8];
char typeflag [1];
char linkname [100];
char magic [6];
char version [2];
char uname [32];
char gname [32];
char devmajor [8];
char devminor [8];
char prefix [155];
char padding [12];
} __attribute__((__packed__));
struct opkg_opk_ustar {
struct opkg_opk_gzip *gzip;
uint64_t data_size_remaining;
struct _opkg_opk_ustar_header header;
char record[OPKG_OPK_USTAR_RECORD_SIZE];
};
struct opkg_opk_ustar *
opkg_opk_ustar_init(struct opkg_opk_gzip *gzip)
{
struct opkg_opk_ustar *ustar;
ustar = malloc(sizeof(*ustar));
if (ustar == NULL) {
return NULL;
}
ustar->gzip = gzip;
ustar->data_size_remaining = 0;
return ustar;
}
static int
_opkg_opk_ustar_next(struct opkg_opk_ustar *ustar)
{
char *end;
uint32_t chksum_got;
uint32_t chksum_exp;
size_t i;
unsigned char *header_uc;
/* Seek through data records until next header record. */
while (ustar->data_size_remaining > 0) {
if (opkg_opk_ustar_read(ustar, NULL, NULL) == OPKG_OPK_ERROR) {
return OPKG_OPK_ERROR;
}
}
/* Decompress next (hopefully valid header) record. */
switch (opkg_opk_gzip_read(ustar->gzip, &ustar->header)) {
case OPKG_OPK_OK:
break;
case OPKG_OPK_END:
/* End of gzip stream before end of ustar archive */
case OPKG_OPK_ERROR:
return OPKG_OPK_ERROR;
}
/* Check for end of archive. */
memset(ustar->record, 0, OPKG_OPK_USTAR_RECORD_SIZE);
if (memcmp(&ustar->header, ustar->record,
OPKG_OPK_USTAR_RECORD_SIZE) == 0) {
return OPKG_OPK_END;
}
/* Verify magic. */
if (memcmp(ustar->header.magic, "ustar", strlen("ustar")) != 0) {
return OPKG_OPK_ERROR;
}
/* Verify checksum. */
/* Assumes chksum is NUL-terminated. Not required by POSIX, but done by
* GNU and BB tar and opkg_opk_ustar_write_header(). */
chksum_got = strtol(ustar->header.chksum, &end,
OPKG_OPK_USTAR_NUM_BASE_);
chksum_exp = 0;
if (*end != '\0') {
return OPKG_OPK_ERROR;
}
for (i = 0; i < sizeof(ustar->header.chksum); ++i) {
ustar->header.chksum[i] = ' ';
}
header_uc = (unsigned char *) &ustar->header;
for (i = 0; i < OPKG_OPK_USTAR_RECORD_SIZE; ++i) {
chksum_exp += header_uc[i];
}
if (chksum_got != chksum_exp) {
return OPKG_OPK_ERROR;
}
/* Depending on type, get size. */
switch (*ustar->header.typeflag) {
case '0': /* Regular file */
case '7': /* High-performance or regular file */
/* Assumes size is NUL-terminated. Not required by
* POSIX, but done by GNU and BB tar and
* opkg_opk_ustar_write_header(). */
ustar->data_size_remaining = strtol(
ustar->header.size, &end,
OPKG_OPK_USTAR_NUM_BASE_);
if (*end != '\0') {
return OPKG_OPK_ERROR;
}
break;
case '2': /* Symbolic link */
case '3': /* Character special file */
case '4': /* Block special file */
case '5': /* Directory */
case '6': /* FIFO special file */
ustar->data_size_remaining = 0;
break;
case '1': /* Link */
default: /* Reserved */
return OPKG_OPK_ERROR; /* Unsupported */
}
return OPKG_OPK_OK;
}
int
opkg_opk_ustar_list(struct opkg_opk_ustar *ustar,
struct opkg_opk_ustar_member **member)
{
int ret;
char *end;
/* Get next header record, if any. */
if ((ret =_opkg_opk_ustar_next(ustar)) != OPKG_OPK_OK) {
return ret; /* Error or end of archive */
}
/* Allocate outward-facing member information structure. */
*member = malloc(sizeof(**member));
if (*member == NULL) {
return OPKG_OPK_ERROR;
}
/* Set name, mode, size, mtime, type, linkname, uname, gname, devmajor,
* and devminor. */
if (ustar->header.prefix[0] != '\0') {
sprintf((*member)->name, "%s/%s",
ustar->header.prefix, ustar->header.name);
} else {
/* Use memcpy() because ustar->header.name may not be
* NUL-terminated. */
memcpy((*member)->name, ustar->header.name,
sizeof(ustar->header.name));
(*member)->name[sizeof(ustar->header.name)] = '\0';
}
/* Assumes mode and mtime are NUL-terminated. Not required by POSIX,
* but done by GNU and BB tar and opkg_opk_ustar_write_header(). */
(*member)->mode = strtol(ustar->header.mode, &end,
OPKG_OPK_USTAR_NUM_BASE_);
if (*end != '\0') {
free(*member);
return OPKG_OPK_ERROR;
}
(*member)->size = ustar->data_size_remaining;
(*member)->mtime = strtol(ustar->header.mtime, &end,
OPKG_OPK_USTAR_NUM_BASE_);
if (*end != '\0') {
free(*member);
return OPKG_OPK_ERROR;
}
switch (*ustar->header.typeflag) {
case '0': /* Regular file */
case '7': /* High-performance or regular file */
(*member)->type = '-';
break;
case '2': /* Symbolic link */
(*member)->type = 'l';
strncpy((*member)->linkname, ustar->header.linkname,
sizeof(ustar->header.linkname));
(*member)->linkname[sizeof((*member)->linkname) - 1] =
'\0';
break;
case '3': /* Character special file */
(*member)->type = 'c';
break;
case '4': /* Block special file */
(*member)->type = 'b';
break;
case '5': /* Directory */
(*member)->type = 'd';
break;
case '6': /* FIFO special file */
(*member)->type = 'p';
break;
case '1': /* Link */
default: /* Reserved */
free(*member);
return OPKG_OPK_ERROR; /* Unsupported */
}
strncpy((*member)->uname, ustar->header.uname,
sizeof((*member)->uname));
strncpy((*member)->gname, ustar->header.gname,
sizeof((*member)->gname));
(*member)->devmajor = strtol(ustar->header.devmajor, &end,
OPKG_OPK_USTAR_NUM_BASE_);
if (*end != '\0') {
free(*member);
return OPKG_OPK_ERROR;
}
(*member)->devminor = strtol(ustar->header.devminor, &end,
OPKG_OPK_USTAR_NUM_BASE_);
if (*end != '\0') {
free(*member);
return OPKG_OPK_ERROR;
}
return OPKG_OPK_OK; /* Possibly more members in archive */
}
int
opkg_opk_ustar_read(struct opkg_opk_ustar *ustar, char **buffer, size_t *size)
{
if (ustar->data_size_remaining == 0) {
return OPKG_OPK_END;
}
/* Decompress next data record. */
switch (opkg_opk_gzip_read(ustar->gzip, ustar->record)) {
case OPKG_OPK_OK:
break;
case OPKG_OPK_END:
case OPKG_OPK_ERROR:
return OPKG_OPK_ERROR;
}
/* Store buffer and size in caller's memory and update remaining size.
*/
if (buffer != NULL) {
*buffer = ustar->record;
}
if (ustar->data_size_remaining >= OPKG_OPK_USTAR_RECORD_SIZE) {
if (size != NULL) {
*size = OPKG_OPK_USTAR_RECORD_SIZE;
}
ustar->data_size_remaining -= OPKG_OPK_USTAR_RECORD_SIZE;
} else {
if (size != NULL) {
*size = ustar->data_size_remaining;
}
ustar->data_size_remaining = 0;
}
return OPKG_OPK_OK;
}
int
opkg_opk_ustar_write_header(struct opkg_opk_ustar *ustar,
struct opkg_opk_dirent *dirent, uint16_t mode,
uid_t uid, const char *uname, gid_t gid, const char *gname,
uint32_t devmajor, uint32_t devminor,
uint64_t size, uint64_t mtime, char type, const char *linkname)
{
uint32_t chksum;
size_t i;
unsigned char *header_uc;
memset(&ustar->header, 0, sizeof(ustar->header));
if (opkg_opk_dirent_name_prefix(dirent, (type == 'd' ? 1 : 0),
ustar->header.name,
sizeof(ustar->header.name),
ustar->header.prefix,
sizeof(ustar->header.prefix)) != OPKG_OPK_OK) {
return OPKG_OPK_ERROR;
}
/* POSIX doesn't say to NUL-terminate mode, uid, gid, size, or mtime.
* GNU and BB tar accept values that aren't (but generate values that
* ARE) NUL-terminated. But more importantly, opkg-lede (see file
* libbb/unarchive.c) parses these with simple strtol() calls, without
* even checking endptr, so we have to NUL-terminate these fields. Also
* parsing these fields with strtol() are our very own
* _opkg_opk_ustar_next() and opkg_opk_ustar_list() above. */
sprintf(ustar->header.mode, "%07o", mode);
sprintf(ustar->header.uid, "%07o", uid);
sprintf(ustar->header.gid, "%07o", gid);
sprintf(ustar->header.size, "%011o", size);
sprintf(ustar->header.mtime, "%011o", mtime);
sprintf(ustar->header.devmajor, "%07o", devmajor);
sprintf(ustar->header.devminor, "%07o", devminor);
switch (type) {
case '-': /* Regular file */
*ustar->header.typeflag = '0';
break;
case 'l': /* Symbolic link */
*ustar->header.typeflag = '2';
strncpy(ustar->header.linkname, linkname,
sizeof(ustar->header.linkname));
break;
case 'c': /* Character special file */
*ustar->header.typeflag = '3';
break;
case 'b': /* Block special file */
*ustar->header.typeflag = '4';
break;
case 'd': /* Directory */
*ustar->header.typeflag = '5';
break;
case 'p': /* FIFO special file */
*ustar->header.typeflag = '6';
break;
default:
return OPKG_OPK_ERROR; /* Unsupported */
}
strncpy(ustar->header.uname, uname, sizeof(ustar->header.uname));
strncpy(ustar->header.gname, gname, sizeof(ustar->header.gname));
/* In these fields POSIX says to write: But GNU and BB tar write: */
strcpy(ustar->header.magic, "ustar"); /* "ustar " */
memcpy(ustar->header.version, "00", 2); /* " \0" i.e. 0x20 0x00 */
/* See files in BusyBox:
* include/bb_archive.h
* archival/chksum_and_xwrite_tar_header.c
* archival/libarchive/get_header_tar.c
* GNU and BB tar accept POSIX-conformant magic and version fields.
* opkg-lede libbb/unarchive.c only checks first 5 bytes of magic and
* ignores version. So conforming to POSIX seems safe.
*/
#if OPKG_OPK_USTAR_BB_EXACT_HEADER_
strcpy(ustar->header.magic, "ustar ");
#endif
chksum = 0;
for (i = 0; i < sizeof(ustar->header.chksum); ++i) {
ustar->header.chksum[i] = ' ';
}
header_uc = (unsigned char *) &ustar->header;
for (i = 0; i < OPKG_OPK_USTAR_RECORD_SIZE; ++i) {
chksum += header_uc[i];
}
/* See above about NUL-terminating and strtol() in opkg-lede. */
/* Since commit 5661fe078eed752780b11f3f4fdd33bbd76a6c5e, BB tar has
* filled chksum with "[6] digits, a null, then a space -- rather than
* digits, followed by a null like the other fields...". The maxiumum
* checksum is (255 x 512 = octal 377000), i.e. a maximum of 6 octal
* digits, so it doesn't matter whether we use 6 or 7 digits. */
#if OPKG_OPK_USTAR_BB_EXACT_HEADER_
sprintf(ustar->header.chksum, "%06o", chksum);
#else
sprintf(ustar->header.chksum, "%07o", chksum);
#endif
if (opkg_opk_gzip_write(ustar->gzip, &ustar->header,
OPKG_OPK_USTAR_RECORD_SIZE) != OPKG_OPK_OK) {
return OPKG_OPK_ERROR;
}
ustar->data_size_remaining = size;
return OPKG_OPK_OK;
}
int
opkg_opk_ustar_get_buffer(struct opkg_opk_ustar *ustar, char **buffer,
size_t *size)
{
*buffer = ustar->record;
*size = sizeof(ustar->record);
return OPKG_OPK_OK;
}
int
opkg_opk_ustar_write_data(struct opkg_opk_ustar *ustar, size_t size)
{
/* Sanity check. */
if (size > ustar->data_size_remaining ||
size > OPKG_OPK_USTAR_RECORD_SIZE) {
return OPKG_OPK_ERROR;
}
/* Zero out end of record. */
memset(ustar->record + size, 0, OPKG_OPK_USTAR_RECORD_SIZE - size);
/* Write to gzip stream. */
if (opkg_opk_gzip_write(ustar->gzip, ustar->record,
OPKG_OPK_USTAR_RECORD_SIZE) != OPKG_OPK_OK) {
return OPKG_OPK_ERROR;
}
ustar->data_size_remaining -= size;
return OPKG_OPK_OK;
}
int
opkg_opk_ustar_write_trailer(struct opkg_opk_ustar *ustar)
{
memset(ustar->record, 0, OPKG_OPK_USTAR_RECORD_SIZE);
if (opkg_opk_gzip_write(ustar->gzip, ustar->record,
OPKG_OPK_USTAR_RECORD_SIZE) != OPKG_OPK_OK ||
opkg_opk_gzip_write(ustar->gzip, ustar->record,
OPKG_OPK_USTAR_RECORD_SIZE) != OPKG_OPK_OK)
{
return OPKG_OPK_ERROR;
}
return OPKG_OPK_OK;
}
void
opkg_opk_ustar_free(struct opkg_opk_ustar *ustar)
{
free(ustar);
}