/* uniso.c - Unpack ISO9660 File System from a stream * * Copyright (C) 2011 Timo Teräs * 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 version 2 as published * by the Free Software Foundation. See http://www.gnu.org/ for details. */ /* Compile with: * gcc -std=gnu99 -D_GNU_SOURCE -D_BSD_SOURCE -O2 uniso.c -o uniso */ /* * TODO: * - fix unaligned 16-bit accesses from iso headers (ARM / MIPS) * - help, options, verbose logs */ /* needed for SPLICE_F_MOVE */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include /**/ #ifndef container_of #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) #endif #define LIST_POISON1 (void *) 0xdeadbeef #define LIST_POISON2 (void *) 0xabbaabba struct list_head { struct list_head *next, *prev; }; static inline void list_init(struct list_head *list) { list->next = list; list->prev = list; } static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; prev->next = next; } static inline void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } static inline int list_hashed(const struct list_head *n) { return n->next != n && n->next != NULL; } #define list_entry(ptr, type, member) container_of(ptr,type,member) /**/ #define ISOFS_BLOCK_SIZE 2048 #define ISOFS_TMPBUF_SIZE (32*ISOFS_BLOCK_SIZE) #if __BYTE_ORDER == __LITTLE_ENDIAN #define endianess le #else #define endianess be #endif struct isofs723 { u_int16_t le; u_int16_t be; }; struct isofs731 { u_int32_t le; }; struct isofs732 { u_int32_t be; }; struct isofs733 { u_int32_t le; u_int32_t be; }; #define ISOFS_VD_PRIMARY 1 #define ISOFS_VD_SUPPLEMENTARY 2 #define ISOFS_VD_END 255 #define ISOFS_STANDARD_ID "CD001" struct isofs_volume_descriptor { unsigned char type; unsigned char id[5]; unsigned char version; unsigned char data[2040]; }; /* Primary or Supplementary Volume Descriptor * (they are about the same, Supplementary just has few more fields * and the date they point to might be encoded differently) */ struct isofs_pri_sup_descriptor { unsigned char type; unsigned char id[5]; unsigned char version; char flags; /* unused1 in primary */ char system_id[32]; char volume_id[32]; char unused2[8]; struct isofs733 volume_space_size; char escape[32]; /* unused3 in primary */ struct isofs723 volume_set_size; struct isofs723 volume_sequence_number; struct isofs723 logical_block_size; struct isofs733 path_table_size; struct isofs731 type_l_path_table, opt_type_l_path_table; struct isofs732 type_m_path_table, opt_type_m_path_table; char root_directory_record[34]; char volume_set_id[128]; char publisher_id[128]; char preparer_id[128]; char application_id[128]; char copyright_file_id[37]; char abstract_file_id[37]; char bibliographic_file_id[37]; char creation_date[17]; char modification_date[17]; char expiration_date[17]; char effective_date[17]; char file_structure_version; char unused4[1]; char application_data[512]; char unused5[653]; }; struct isofs_directory_record { unsigned char length; unsigned char ext_attr_length; struct isofs733 extent; struct isofs733 size; char date[7]; char flags; #define ISOFS_DR_FLAG_DIRECTORY (1<<1) unsigned char file_unit_size; unsigned char interleave; struct isofs723 volume_sequence_number; unsigned char name_len; char name[0]; } __attribute__((packed)); struct uniso_reader; struct uniso_context; typedef int (*uniso_handler_f)(struct uniso_context *, struct uniso_reader *); struct uniso_reader { struct list_head parser_list; size_t offset; uniso_handler_f handler; }; struct uniso_dirent { struct uniso_reader reader; u_int32_t size; u_int32_t flags; char name[0]; }; struct uniso_context { int stream_fd, null_fd; int skip_method, copy_method; int joliet_level, loglevel; size_t pos, last_queued_read; struct list_head parser_head; struct uniso_reader volume_desc_reader; unsigned char *tmpbuf; }; static int do_splice(struct uniso_context *ctx, int to_fd, int bytes) { int r; do { r = splice(ctx->stream_fd, NULL, to_fd, NULL, bytes, SPLICE_F_MOVE); if (r < 0) return -errno; bytes -= r; ctx->pos += r; } while (bytes != 0); return 0; } static int do_read(struct uniso_context *ctx, unsigned char *buf, int bytes) { int r; do { r = read(ctx->stream_fd, buf, bytes); if (r < 0) { perror("read"); return -errno; } bytes -= r; buf += r; ctx->pos += r; } while (bytes != 0); return 0; } static int do_write(int tofd, const unsigned char *buf, int bytes) { int r; do { r = write(tofd, buf, bytes); if (r < 0) { perror("write"); return -errno; } bytes -= r; buf += r; } while (bytes != 0); return 0; } static int do_skip(struct uniso_context *ctx, unsigned int bytes) { int r, now; switch (ctx->skip_method) { case 0: if (lseek(ctx->stream_fd, bytes, SEEK_CUR) != (off_t) -1) { ctx->pos += bytes; break; } ctx->skip_method = 1; case 1: if (do_splice(ctx, ctx->null_fd, bytes) == 0) break; ctx->skip_method = 2; case 2: while (bytes) { now = bytes; if (now > ISOFS_TMPBUF_SIZE) now = ISOFS_TMPBUF_SIZE; r = do_read(ctx, ctx->tmpbuf, now); if (r < 0) return r; bytes -= now; } break; } return 0; } static int do_copy(struct uniso_context *ctx, int tofd, unsigned int bytes) { int r, now; switch (ctx->copy_method) { case 0: if (do_splice(ctx, tofd, bytes) == 0) break; ctx->skip_method = 1; case 1: /* FIXME: Use mmaped IO */ ctx->skip_method = 2; case 2: while (bytes) { now = bytes; if (now > ISOFS_TMPBUF_SIZE) now = ISOFS_TMPBUF_SIZE; r = do_read(ctx, ctx->tmpbuf, now); if (r < 0) return r; r = do_write(tofd, ctx->tmpbuf, now); if (r < 0) return r; bytes -= now; } break; } return 0; } static int queue_reader( struct uniso_context *ctx, struct uniso_reader *reader, size_t offset, uniso_handler_f handler) { struct list_head *n; struct uniso_reader *r; if (offset < ctx->pos) { fprintf(stderr, "ERROR: non-linear reads are not supported " "(asked %x, we are at %x)\n", offset, ctx->last_queued_read); return -1; } reader->offset = offset; reader->handler = handler; list_init(&reader->parser_list); if (offset >= ctx->last_queued_read || !list_hashed(&ctx->parser_head)) { ctx->last_queued_read = offset; list_add_tail(&reader->parser_list, &ctx->parser_head); return 0; } for (n = ctx->parser_head.prev; n != &ctx->parser_head; n = n->prev) { r = list_entry(n, struct uniso_reader, parser_list); if (r->offset < offset) break; } list_add(&reader->parser_list, n); return 0; } static int queue_dirent(struct uniso_context *ctx, void *isode, const char *name); static int uniso_read_file(struct uniso_context *ctx, struct uniso_reader *rd) { struct uniso_dirent *dir = container_of(rd, struct uniso_dirent, reader); int fd, rc; // FIXME: seems that hardlinked files get the same file extent // shared. need to fix extraction of such files. if (ctx->pos > rd->offset) { if (ctx->loglevel > 0) fprintf(stderr, "WARNING: Not extracting '%s' as " "it's sharing file-extents with " "another file\n", dir->name); return 0; } fd = open(dir->name, O_WRONLY | O_TRUNC | O_CREAT, 0777); if (fd < 0) return -errno; if (ctx->loglevel > 1) fprintf(stderr, "%s : %d bytes, flags 0x%08x\n", dir->name, dir->size, dir->flags); rc = do_copy(ctx, fd, dir->size); close(fd); return rc; } static int uniso_read_directory(struct uniso_context *ctx, struct uniso_reader *rd) { struct uniso_dirent *dir = container_of(rd, struct uniso_dirent, reader); struct isofs_directory_record *ide; char buf[512], *name; char *tmp; int r, i, offs; if (ctx->loglevel > 1) fprintf(stderr, "%s : DIR, flags 0x%08x\n", dir->name, dir->flags); mkdir(dir->name, 0777); tmp = malloc(dir->size); if (tmp == NULL) { r = -ENOMEM; goto ret_nomem; } r = do_read(ctx, tmp, dir->size); if (r < 0) goto ret; strcpy(buf, dir->name); strcat(buf, "/"); for (offs = 0; offs < dir->size; ) { ide = (void *) &tmp[offs]; offs += (ide->length + 1) & ~1; if (ide->length == 0) { offs &= ~(ISOFS_BLOCK_SIZE-1); offs += ISOFS_BLOCK_SIZE; continue; } /* Skip '.' and '..' entries */ if ((ide->name_len == 0) || (ide->name_len == 1 && ide->name[0] <= 1)) continue; name = &buf[strlen(dir->name) + 1]; if (ctx->joliet_level) { u_int16_t *wname = (u_int16_t *) ide->name; for (i = 0; i < ide->name_len; i += 2, wname++) { r = wctomb(name, be16toh(*wname)); if (r == -1) *name++ = '?'; else name += r; } *name = 0; } else { memcpy(name, ide->name, ide->name_len); name[ide->name_len] = 0; } queue_dirent(ctx, ide, buf); } r = 0; ret: free(tmp); ret_nomem: free(dir); return r; } static int queue_dirent(struct uniso_context *ctx, void *isode, const char *name) { struct isofs_directory_record *ide = isode; struct uniso_dirent *dir; dir = calloc(1, sizeof(*dir) + strlen(name) + 1); if (dir == NULL) return -ENOMEM; dir->size = ide->size.endianess; dir->flags = ide->flags; strcpy(dir->name, name); return queue_reader(ctx, &dir->reader, ide->extent.endianess * ISOFS_BLOCK_SIZE, (ide->flags & ISOFS_DR_FLAG_DIRECTORY) ? uniso_read_directory : uniso_read_file); } static int uniso_read_volume_descriptor(struct uniso_context *ctx, struct uniso_reader *rd) { char buf[ISOFS_BLOCK_SIZE]; struct isofs_volume_descriptor *vd = (void*) buf; struct isofs_pri_sup_descriptor *pd = (void*) buf; char root_dir[sizeof(pd->root_directory_record)]; int r; do { r = do_read(ctx, buf, ISOFS_BLOCK_SIZE); if (r < 0) return r; if (memcmp(vd->id, ISOFS_STANDARD_ID, sizeof(vd->id)) != 0) return -EMEDIUMTYPE; switch (vd->type) { case ISOFS_VD_PRIMARY: if (ctx->joliet_level == 0) memcpy(root_dir, pd->root_directory_record, sizeof(root_dir)); break; case ISOFS_VD_SUPPLEMENTARY: if (pd->escape[0] == 0x25 && pd->escape[1] == 0x2f) { switch (pd->escape[2]) { case 0x45: ctx->joliet_level++; case 0x43: ctx->joliet_level++; case 0x40: ctx->joliet_level++; memcpy(root_dir, pd->root_directory_record, sizeof(root_dir)); } } break; } } while (vd->type != ISOFS_VD_END); if (ctx->joliet_level && ctx->loglevel > 1) fprintf(stderr, "INFO: Microsoft Joliet Level %d\n", ctx->joliet_level); return queue_dirent(ctx, root_dir, "."); } int uniso(int fd) { struct uniso_context context, *ctx = &context; struct uniso_reader *rd; int r; memset(ctx, 0, sizeof(*ctx)); list_init(&ctx->parser_head); ctx->stream_fd = fd; ctx->null_fd = open("/dev/null", O_RDWR); ctx->tmpbuf = malloc(ISOFS_TMPBUF_SIZE); ctx->loglevel = 1; queue_reader(ctx, &ctx->volume_desc_reader, 16 * ISOFS_BLOCK_SIZE, uniso_read_volume_descriptor); while (list_hashed(&ctx->parser_head)) { rd = list_entry(ctx->parser_head.next, struct uniso_reader, parser_list); list_del(&rd->parser_list); r = rd->offset - ctx->pos; if (r > 0) { if ((r = do_skip(ctx, r)) < 0) return r; } r = rd->handler(ctx, rd); if (r != 0) return r; } free(ctx->tmpbuf); close(ctx->null_fd); } int main(void) { uniso(STDIN_FILENO); return 0; }