blob: 47d548909dc28db219089bc92c67cdbabdf5d9c7 [file] [log] [blame]
/* ----------------------------------------------------------------------- *
*
* Copyright 2001 H. Peter Anvin - All Rights Reserved
*
* This program 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, Inc., 675 Mass Ave, Cambridge MA 02139,
* USA; either version 2 of the License, or (at your option) any later
* version; incorporated herein by reference.
*
* ----------------------------------------------------------------------- */
/*
* mkzffile.c
*
* - Generate block-compression of files for use with
* the "ZF" extension to the iso9660/RockRidge filesystem.
*
* The file compression technique used is the "deflate"
* algorithm used by the zlib library; each block must have a
* valid (12-byte) zlib header. In addition, the file itself
* has the following structure:
*
* Byte offset iso9660 type Contents
* 0 (8 bytes) Magic number (37 E4 53 96 C9 DB D6 07)
* 8 7.3.1 Uncompressed file size
* 12 7.1.1 header_size >> 2 (currently 4)
* 13 7.1.1 log2(block_size)
* 14 (2 bytes) Reserved, must be zero
*
* The header may get expanded in the future, at which point the
* header size field will be used to increase the space for the
* header.
*
* All implementations are required to support a block_size of 32K
* (byte 13 == 15).
*
* Note that bytes 12 and 13 and the uncompressed length are also
* present in the ZF record; THE TWO MUST BOTH BE CONSISTENT AND
* CORRECT.
*
* Given the uncompressed size, block_size, and header_size:
*
* nblocks := ceil(size/block_size)
*
* After the header follow (nblock+1) 32-bit pointers, recorded as
* iso9660 7.3.1 (littleendian); each indicate the byte offset (from
* the start of the file) to one block and the first byte beyond the
* end of the previous block; the first pointer thus point to the
* start of the data area and the last pointer to the first byte
* beyond it:
*
* block_no := floor(byte_offset/block_size)
*
* block_start := read_pointer_731( (header_size+block_no)*4 )
* block_end := read_pointer_731( (header_size+block_no+1)*4 )
*
* The block data is compressed according to "zlib".
*/
#include <dirent.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <utime.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <zlib.h>
#define HAVE_LCHOWN 1 /* Should be obtained by autoconf or so */
/* Command line options */
int force = 0; /* Always compress */
int level = 9; /* Compression level */
int verbosity = 0;
/* Program name */
const char *program;
/* Convenience functions */
void *xmalloc(size_t size)
{
void *p = malloc(size);
if ( !p ) {
perror(program);
exit(1);
}
return p;
}
char *xstrdup(const char *str)
{
char *s = strdup(str);
if ( !s ) {
perror(program);
exit(1);
}
return s;
}
static void
set_721(void *pnt, unsigned int i)
{
unsigned char *p = (unsigned char *)pnt;
p[0] = i & 0xff;
p[1] = (i >> 8) & 0xff;
}
static unsigned int
get_721(void *pnt)
{
unsigned char *p = (unsigned char *)pnt;
return ((unsigned int)p[0]) + ((unsigned int)p[1] << 8);
}
static void
set_722(void *pnt, unsigned int i)
{
unsigned char *p = (unsigned char *)pnt;
p[0] = (i >> 8) & 0xff;
p[1] = i & 0xff;
}
static unsigned int
get_722(void *pnt)
{
unsigned char *p = (unsigned char *)pnt;
return ((unsigned int)p[0] << 8) + ((unsigned int)p[1]);
}
static void
set_723(void *pnt, unsigned int i)
{
unsigned char *p = (unsigned char *)pnt;
p[3] = p[0] = i & 0xff;
p[2] = p[1] = (i >> 8) & 0xff;
}
#define get_723(x) get_721(x)
static void
set_731(void *pnt, unsigned int i)
{
unsigned char *p = (unsigned char *)pnt;
p[0] = i & 0xff;
p[1] = (i >> 8) & 0xff;
p[2] = (i >> 16) & 0xff;
p[3] = (i >> 24) & 0xff;
}
static unsigned int
get_731(void *pnt)
{
unsigned char *p = (unsigned char *)pnt;
return ((unsigned int)p[0]) + ((unsigned int)p[1] << 8) +
((unsigned int)p[2] << 16) + ((unsigned int)p[3] << 24);
}
static void
set_732(void *pnt, unsigned int i)
{
unsigned char *p = (unsigned char *)pnt;
p[3] = i & 0xff;
p[2] = (i >> 8) & 0xff;
p[1] = (i >> 16) & 0xff;
p[0] = (i >> 24) & 0xff;
}
static unsigned int
get_732(void *pnt)
{
unsigned char *p = (unsigned char *)pnt;
return ((unsigned int)p[0] << 24) + ((unsigned int)p[1] << 16) +
((unsigned int)p[2] << 8) + ((unsigned int)p[3]);
}
static void
set_733(void *pnt, unsigned int i)
{
unsigned char *p = (unsigned char *)pnt;
p[7] = p[0] = i & 0xff;
p[6] = p[1] = (i >> 8) & 0xff;
p[5] = p[2] = (i >> 16) & 0xff;
p[4] = p[3] = (i >> 24) & 0xff;
}
#define get_733(x) get_731(x)
/* File transformation function */
typedef int (*munger_func)(FILE *, FILE *, unsigned long);
/* zisofs definitions */
#ifndef CBLOCK_SIZE_LG2
#define CBLOCK_SIZE_LG2 15 /* Compressed block size */
#endif
#define CBLOCK_SIZE (1U << CBLOCK_SIZE_LG2)
/* Compressed file magic */
const unsigned char zisofs_magic[8] =
{ 0x37, 0xE4, 0x53, 0x96, 0xC9, 0xDB, 0xD6, 0x07 };
/* VERY VERY VERY IMPORTANT: Must be a multiple of 4 bytes */
struct compressed_file_header {
char magic[8];
char uncompressed_len[4];
unsigned char header_size;
unsigned char block_size;
char reserved[2]; /* Reserved for future use, MBZ */
};
int block_uncompress_file(FILE *input, FILE *output, unsigned long size)
{
struct compressed_file_header hdr;
char *inbuf, *outbuf;
long bytes;
int block_shift;
char *pointer_block, *pptr;
unsigned long position;
unsigned long nblocks;
unsigned long fullsize, block_size, block_size2;
unsigned long ptrblock_bytes;
unsigned long cstart, cend, csize;
int zerr;
int err = -1;
if ( (bytes = fread(&hdr, 1, sizeof hdr, input)) != sizeof hdr ) {
if ( bytes == size ) {
/* Very short file; not compressed */
return ( fwrite(&hdr, 1, bytes, output) != bytes ) ? -1 : 0;
} else {
return -1; /* Read error */
}
}
if ( memcmp(&hdr.magic, zisofs_magic, sizeof zisofs_magic) ) {
inbuf = xmalloc(CBLOCK_SIZE);
/* Not compressed */
memcpy(inbuf, &hdr, sizeof hdr);
bytes = sizeof hdr;
do {
if ( fwrite(inbuf, 1, bytes, output) != bytes )
return -1;
} while ( (bytes = fread(inbuf, 1, CBLOCK_SIZE, input)) > 0 );
free(inbuf);
return (bytes < 0) ? -1 : 0;
}
/* Now we know the file must be compressed. Get the pointer table. */
if ( fseek(input, hdr.header_size << 2, SEEK_SET) == -1 )
return -1;
fullsize = get_731(hdr.uncompressed_len);
block_shift = hdr.block_size;
block_size = 1UL << block_shift;
block_size2 = block_size << 1;
inbuf = xmalloc(block_size2);
outbuf = xmalloc(block_size);
nblocks = (fullsize + block_size - 1) >> block_shift;
ptrblock_bytes = (nblocks+1) * 4;
pointer_block = xmalloc(ptrblock_bytes);
errno = 0;
if ( (bytes = fread(pointer_block, 1, ptrblock_bytes, input)) != ptrblock_bytes ) {
if ( errno == 0 ) errno = EINVAL;
goto free_ptr_bail;
}
pptr = pointer_block;
while ( fullsize ) {
cstart = get_731(pptr);
pptr += 4;
cend = get_731(pptr);
csize = cend-cstart;
if ( csize == 0 ) {
memset(outbuf, 0, block_size);
bytes = block_size;
} else {
if ( csize > block_size2 ) {
errno = EINVAL;
goto free_ptr_bail;
}
if ( fseek(input, cstart, SEEK_SET) == -1 )
goto free_ptr_bail;
errno = 0;
if ( (bytes = fread(inbuf, 1, csize, input)) != csize ) {
if ( errno == 0 ) errno = EINVAL;
goto free_ptr_bail;
}
bytes = block_size; /* Max output buffer size */
if ( (zerr = uncompress(outbuf, &bytes, inbuf, csize)) != Z_OK ) {
errno = (zerr = Z_MEM_ERROR) ? ENOMEM : EINVAL;
goto free_ptr_bail;
}
}
if ( ((fullsize > block_size) && (bytes != block_size))
|| ((fullsize <= block_size) && (bytes < fullsize)) ) {
errno = EINVAL;
goto free_ptr_bail;
}
if ( bytes > fullsize )
bytes = fullsize;
errno = 0;
if ( fwrite(outbuf, 1, bytes, output) != bytes ) {
if ( errno == 0 ) errno = EINVAL;
goto free_ptr_bail;
}
fullsize -= bytes;
}
err = 0;
free_ptr_bail:
free(pointer_block);
free(inbuf);
free(outbuf);
return err;
}
int block_compress_file(FILE *input, FILE *output, unsigned long size)
{
struct compressed_file_header hdr;
char inbuf[CBLOCK_SIZE], outbuf[2*CBLOCK_SIZE];
int bytes, pointer_bytes, nblocks, block;
uLong cbytes; /* uLong is a zlib datatype */
char *pointer_block, *curptr;
unsigned long position;
int i;
int header_size;
int force_compress = force;
int zerr;
if ( (sizeof hdr) & 3 ) {
fputs("INTERNAL ERROR: header is not a multiple of 4\n", stderr);
abort();
}
memset(&hdr, 0, sizeof hdr);
memcpy(&hdr.magic, zisofs_magic, sizeof zisofs_magic);
hdr.header_size = (sizeof hdr) >> 2;
hdr.block_size = CBLOCK_SIZE_LG2;
set_731(&hdr.uncompressed_len, size);
if ( fwrite(&hdr, sizeof hdr, 1, output) != 1 )
return -1;
nblocks = (size+CBLOCK_SIZE-1) >> CBLOCK_SIZE_LG2;
pointer_bytes = 4*(nblocks+1);
pointer_block = xmalloc(pointer_bytes);
if ( !pointer_block )
return -1;
memset(pointer_block, 0, pointer_bytes);
if ( fseek(output, pointer_bytes, SEEK_CUR) == -1 )
goto free_ptr_bail;
curptr = pointer_block;
position = sizeof hdr + pointer_bytes;
block = 0;
while ( (bytes = fread(inbuf, 1, CBLOCK_SIZE, input)) > 0 ) {
if ( bytes < CBLOCK_SIZE && block < nblocks-1 ) {
errno = EINVAL; /* Someone changed the file on us */
goto free_ptr_bail;
}
/* HACK: If the file has our magic number, always compress */
if ( block == 0 && bytes >= sizeof zisofs_magic ) {
if ( !memcmp(inbuf, zisofs_magic, sizeof zisofs_magic) )
force_compress = 1;
}
set_731(curptr, position); curptr += 4;
/* We have two special cases: a zero-length block is defined as all zero,
and a block the length of which is equal to the block size is unencoded. */
for ( i = 0 ; i < CBLOCK_SIZE ; i++ ) {
if ( inbuf[i] ) break;
}
if ( i == CBLOCK_SIZE ) {
/* All-zero block. No output */
} else {
cbytes = 2*CBLOCK_SIZE;
if ( (zerr = compress2(outbuf, &cbytes, inbuf, bytes, level)) != Z_OK ) {
errno = (zerr == Z_MEM_ERROR) ? ENOMEM : EINVAL;
goto free_ptr_bail; /* Compression failure */
}
if ( fwrite(outbuf, 1, cbytes, output) != cbytes )
goto free_ptr_bail;
position += cbytes;
}
block++;
}
/* Set pointer to the end of the final block */
set_731(curptr, position);
/* Now write the pointer table */
if ( fseek(output, sizeof hdr, SEEK_SET) == -1 )
goto free_ptr_bail;
if ( fwrite(pointer_block, 1, pointer_bytes, output) != pointer_bytes )
goto free_ptr_bail;
free(pointer_block);
/* Now make sure that this was actually the right thing to do */
if ( !force_compress && position >= size ) {
/* Incompressible file, just copy it */
rewind(input);
rewind(output);
position = 0;
while ( (bytes = fread(inbuf, 1, CBLOCK_SIZE, input)) > 0 ) {
if ( fwrite(inbuf, 1, bytes, output) != bytes )
return -1;
position += bytes;
}
/* Truncate the file to the correct size */
fflush(output);
ftruncate(fileno(output), position);
}
/* If we get here, we're done! */
return 0;
/* Common bailout code */
free_ptr_bail:
free(pointer_block);
return -1;
}
int munge_path(const char *inpath, const char *outpath, unsigned long size, munger_func munger)
{
FILE *in, *out;
int err, rv;
in = fopen(inpath, "rb");
if ( !in )
return -1;
out = fopen(outpath, "wb");
if ( !out ) {
err = errno;
fclose(in);
errno = err;
return -1;
}
rv = munger(in, out, size);
err = errno; /* Just in case */
fclose(in);
fclose(out);
errno = err;
return rv;
}
/* Hash table used to find hard-linked files */
#define HASH_BUCKETS 2683
struct file_hash {
struct file_hash *next;
struct stat st;
const char *outfile_name;
};
static struct file_hash *hashp[HASH_BUCKETS];
const char *hash_find_file(struct stat *st)
{
int bucket = (st->st_ino + st->st_dev) % HASH_BUCKETS;
struct file_hash *hp;
for ( hp = hashp[bucket] ; hp ; hp = hp->next ) {
if ( hp->st.st_ino == st->st_ino &&
hp->st.st_dev == st->st_dev &&
hp->st.st_mode == st->st_mode &&
hp->st.st_nlink == st->st_nlink &&
hp->st.st_uid == st->st_uid &&
hp->st.st_gid == st->st_gid &&
hp->st.st_size == st->st_size &&
hp->st.st_mtime == st->st_mtime ) {
/* Good enough, it's the same file */
return hp->outfile_name;
}
}
return NULL; /* No match */
}
/* Note: the stat structure is the input file; the name
is the output file to link to */
void hash_insert_file(struct stat *st, const char *outfile)
{
int bucket = (st->st_ino + st->st_dev) % HASH_BUCKETS;
struct file_hash *hp = xmalloc(sizeof(struct file_hash));
hp->next = hashp[bucket];
memcpy(&hp->st, st, sizeof(struct stat));
hp->outfile_name = xstrdup(outfile);
hashp[bucket] = hp;
}
int munge_tree(const char *intree, const char *outtree, munger_func munger)
{
char buffer[BUFSIZ];
char *in_path, *out_path, *in_file, *out_file;
DIR *thisdir;
struct dirent *dirent;
struct stat st;
struct utimbuf ut;
int err = 0;
/* Construct buffers with the common filename prefix, and point to the end */
in_path = xmalloc(strlen(intree) + NAME_MAX + 2);
out_path = xmalloc(strlen(outtree) + NAME_MAX + 2);
strcpy(in_path, intree);
strcpy(out_path, outtree);
in_file = strchr(in_path, '\0');
out_file = strchr(out_path, '\0');
*in_file++ = '/';
*out_file++ = '/';
/* Open the directory */
thisdir = opendir(intree);
if ( !thisdir ) {
fprintf(stderr, "%s: Failed to open directory %s: %s\n",
program, intree, strerror(errno));
return 1;
}
/* Create output directory */
if ( mkdir(outtree, 0700) ) {
fprintf(stderr, "%s: Cannot create output directory %s: %s\n",
program, outtree, strerror(errno));
return 1;
}
while ( (dirent = readdir(thisdir)) != NULL ) {
if ( !strcmp(dirent->d_name, ".") ||
!strcmp(dirent->d_name, "..") )
continue; /* Ignore . and .. */
strcpy(in_file, dirent->d_name);
strcpy(out_file, dirent->d_name);
if ( lstat(in_path, &st) ) {
fprintf(stderr, "%s: Failed to stat file %s: %s\n",
program, in_path, strerror(errno));
err = 1;
break;
}
if ( S_ISREG(st.st_mode) ) {
if ( st.st_nlink > 1 ) {
/* Hard link. */
const char *linkname;
if ( (linkname = hash_find_file(&st)) != NULL ) {
/* We've seen it before, hard link it */
if ( link(linkname, out_path) ) {
fprintf(stderr, "%s: hard link %s -> %s failed: %s\n",
program, out_path, linkname, strerror(errno));
err = 1;
break;
}
} else {
/* First encounter, compress and enter into hash */
if ( munge_path(in_path, out_path, st.st_size, munger) ) {
fprintf(stderr, "%s: %s: %s", program, in_path, strerror(errno));
err = 1;
break;
}
hash_insert_file(&st, out_path);
}
} else {
/* Singleton file; no funnies */
if ( munge_path(in_path, out_path, st.st_size, munger) ) {
fprintf(stderr, "%s: %s: %s", program, in_path, strerror(errno));
err = 1;
break;
}
}
} else if ( S_ISDIR(st.st_mode) ) {
/* Recursion: see recursion */
err = munge_tree(in_path, out_path, munger);
if ( err )
break;
} else if ( S_ISLNK(st.st_mode) ) {
int chars;
if ( (chars = readlink(in_path, buffer, BUFSIZ)) < 0 ) {
fprintf(stderr, "%s: readlink failed for %s: %s\n",
program, in_path, strerror(errno));
err = 1;
break;
}
buffer[chars] = '\0';
if ( symlink(buffer, out_path) ) {
fprintf(stderr, "%s: symlink %s -> %s failed: %s\n",
program, out_path, buffer, strerror(errno));
err = 1;
break;
}
} else {
if ( st.st_nlink > 1 ) {
/* Hard link. */
const char *linkname;
if ( (linkname = hash_find_file(&st)) != NULL ) {
/* We've seen it before, hard link it */
if ( link(linkname, out_path) ) {
fprintf(stderr, "%s: hard link %s -> %s failed: %s\n",
program, out_path, linkname, strerror(errno));
err = 1;
break;
}
} else {
/* First encounter, create and enter into hash */
if ( mknod(out_path, st.st_mode, st.st_rdev) ) {
fprintf(stderr, "%s: mknod failed for %s: %s\n",
program, out_path, strerror(errno));
err = 1;
break;
}
hash_insert_file(&st, out_path);
}
} else {
/* Singleton node; no funnies */
if ( mknod(out_path, st.st_mode, st.st_rdev) ) {
fprintf(stderr, "%s: mknod failed for %s: %s\n",
program, out_path, strerror(errno));
err = 1;
break;
}
}
}
#ifdef HAVE_LCHOWN
lchown(out_path, st.st_uid, st.st_gid);
#endif
if ( !S_ISLNK(st.st_mode) ) {
#ifndef HAVE_LCHOWN
chown(out_path, st.st_uid, st.st_gid);
#endif
chmod(out_path, st.st_mode);
ut.actime = st.st_atime;
ut.modtime = st.st_mtime;
utime(out_path, &ut);
}
}
closedir(thisdir);
free(in_path);
free(out_path);
return err;
}
static void usage(int err)
{
fprintf(stderr,
"Usage: %s [-vfhu] [-z level] intree outtree\n",
program);
exit(err);
}
int main(int argc, char *argv[])
{
const char *in, *out;
struct stat st;
struct utimbuf ut;
int opt, err;
munger_func munger = block_compress_file;
program = argv[0];
while ( (opt = getopt(argc, argv, "vfz:hu")) != EOF ) {
switch(opt) {
case 'f':
force = 1; /* Always compress */
break;
case 'z':
if ( optarg[0] < '0' || optarg[0] > '9' || optarg[1] ) {
fprintf(stderr, "%s: invalid compression level: %s\n",
program, optarg);
exit(1);
} else {
level = optarg[0] - '0';
}
break;
case 'h':
usage(0);
break;
case 'v':
verbosity++;
break;
case 'u':
munger = block_uncompress_file;
break;
default:
usage(1);
break;
}
}
if ( (argc-optind) != 2 )
usage(1);
in = argv[optind]; /* Input tree */
out = argv[optind+1]; /* Output tree */
umask(077);
/* Special case: we use stat() for the root, not lstat() */
if ( stat(in, &st) ) {
fprintf(stderr, "%s: %s: %s\n", program, in, strerror(errno));
exit(1);
}
if ( !S_ISDIR(st.st_mode) ) {
fprintf(stderr, "%s: %s: Not a directory\n", program, in);
}
err = munge_tree(in, out, munger);
if ( err )
exit(err);
chown(out, st.st_uid, st.st_gid);
chmod(out, st.st_mode);
ut.actime = st.st_atime;
ut.modtime = st.st_mtime;
utime(out, &ut);
}