/* affs.c - Amiga Fast FileSystem. */ /* * GRUB -- GRand Unified Bootloader * Copyright (C) 2005,2006,2007,2008,2009 Free Software Foundation, Inc. * * GRUB 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. * * GRUB 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 GRUB. If not, see . */ #include #include #include #include #include #include #include #include #include GRUB_MOD_LICENSE ("GPLv3+"); /* The affs bootblock. */ struct grub_affs_bblock { grub_uint8_t type[3]; grub_uint8_t flags; grub_uint32_t checksum; grub_uint32_t rootblock; } GRUB_PACKED; /* Set if the filesystem is a AFFS filesystem. Otherwise this is an OFS filesystem. */ #define GRUB_AFFS_FLAG_FFS 1 /* The affs rootblock. */ struct grub_affs_rblock { grub_uint32_t type; grub_uint8_t unused1[8]; grub_uint32_t htsize; grub_uint32_t unused2; grub_uint32_t checksum; grub_uint32_t hashtable[1]; } GRUB_PACKED; struct grub_affs_time { grub_int32_t day; grub_uint32_t min; grub_uint32_t hz; } GRUB_PACKED; /* The second part of a file header block. */ struct grub_affs_file { grub_uint8_t unused1[12]; grub_uint32_t size; grub_uint8_t unused2[92]; struct grub_affs_time mtime; grub_uint8_t namelen; grub_uint8_t name[30]; grub_uint8_t unused3[5]; grub_uint32_t hardlink; grub_uint32_t unused4[6]; grub_uint32_t next; grub_uint32_t parent; grub_uint32_t extension; grub_uint32_t type; } GRUB_PACKED; /* The location of `struct grub_affs_file' relative to the end of a file header block. */ #define GRUB_AFFS_FILE_LOCATION 200 /* The offset in both the rootblock and the file header block for the hashtable, symlink and block pointers (all synonyms). */ #define GRUB_AFFS_HASHTABLE_OFFSET 24 #define GRUB_AFFS_BLOCKPTR_OFFSET 24 #define GRUB_AFFS_SYMLINK_OFFSET 24 enum { GRUB_AFFS_FILETYPE_DIR = 2, GRUB_AFFS_FILETYPE_SYMLINK = 3, GRUB_AFFS_FILETYPE_HARDLINK = 0xfffffffc, GRUB_AFFS_FILETYPE_REG = 0xfffffffd }; #define AFFS_MAX_LOG_BLOCK_SIZE 4 #define AFFS_MAX_SUPERBLOCK 1 struct grub_fshelp_node { struct grub_affs_data *data; grub_uint32_t block; struct grub_fshelp_node *parent; struct grub_affs_file di; grub_uint32_t *block_cache; grub_uint32_t last_block_cache; }; /* Information about a "mounted" affs filesystem. */ struct grub_affs_data { struct grub_affs_bblock bblock; struct grub_fshelp_node diropen; grub_disk_t disk; /* Log blocksize in sectors. */ int log_blocksize; /* The number of entries in the hashtable. */ unsigned int htsize; }; static grub_dl_t my_mod; static grub_disk_addr_t grub_affs_read_block (grub_fshelp_node_t node, grub_disk_addr_t fileblock) { grub_uint32_t target, curblock; grub_uint32_t pos; struct grub_affs_file file; struct grub_affs_data *data = node->data; grub_uint64_t mod; if (!node->block_cache) { node->block_cache = grub_malloc (((grub_be_to_cpu32 (node->di.size) >> (9 + node->data->log_blocksize)) / data->htsize + 2) * sizeof (node->block_cache[0])); if (!node->block_cache) return -1; node->last_block_cache = 0; node->block_cache[0] = node->block; } /* Files are at most 2G on AFFS, so no need for 64-bit division. */ target = (grub_uint32_t) fileblock / data->htsize; mod = (grub_uint32_t) fileblock % data->htsize; /* Find the block that points to the fileblock we are looking up by following the chain until the right table is reached. */ for (curblock = node->last_block_cache + 1; curblock < target + 1; curblock++) { grub_disk_read (data->disk, (((grub_uint64_t) node->block_cache[curblock - 1] + 1) << data->log_blocksize) - 1, GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION, sizeof (file), &file); if (grub_errno) return 0; node->block_cache[curblock] = grub_be_to_cpu32 (file.extension); node->last_block_cache = curblock; } /* Translate the fileblock to the block within the right table. */ grub_disk_read (data->disk, (grub_uint64_t) node->block_cache[target] << data->log_blocksize, GRUB_AFFS_BLOCKPTR_OFFSET + (data->htsize - mod - 1) * sizeof (pos), sizeof (pos), &pos); if (grub_errno) return 0; return grub_be_to_cpu32 (pos); } static struct grub_affs_data * grub_affs_mount (grub_disk_t disk) { struct grub_affs_data *data; grub_uint32_t *rootblock = 0; struct grub_affs_rblock *rblock = 0; int log_blocksize = 0; int bsnum = 0; data = grub_zalloc (sizeof (struct grub_affs_data)); if (!data) return 0; for (bsnum = 0; bsnum < AFFS_MAX_SUPERBLOCK + 1; bsnum++) { /* Read the bootblock. */ grub_disk_read (disk, bsnum, 0, sizeof (struct grub_affs_bblock), &data->bblock); if (grub_errno) goto fail; /* Make sure this is an affs filesystem. */ if (grub_strncmp ((char *) (data->bblock.type), "DOS", 3) != 0 /* Test if the filesystem is a OFS filesystem. */ || !(data->bblock.flags & GRUB_AFFS_FLAG_FFS)) continue; /* No sane person uses more than 8KB for a block. At least I hope for that person because in that case this won't work. */ if (!rootblock) rootblock = grub_malloc (GRUB_DISK_SECTOR_SIZE << AFFS_MAX_LOG_BLOCK_SIZE); if (!rootblock) goto fail; rblock = (struct grub_affs_rblock *) rootblock; /* The filesystem blocksize is not stored anywhere in the filesystem itself. One way to determine it is try reading blocks for the rootblock until the checksum is correct. */ for (log_blocksize = 0; log_blocksize <= AFFS_MAX_LOG_BLOCK_SIZE; log_blocksize++) { grub_uint32_t *currblock = rootblock; unsigned int i; grub_uint32_t checksum = 0; /* Read the rootblock. */ grub_disk_read (disk, (grub_uint64_t) grub_be_to_cpu32 (data->bblock.rootblock) << log_blocksize, 0, GRUB_DISK_SECTOR_SIZE << log_blocksize, rootblock); if (grub_errno == GRUB_ERR_OUT_OF_RANGE) { grub_errno = 0; break; } if (grub_errno) goto fail; if (rblock->type != grub_cpu_to_be32_compile_time (2) || rblock->htsize == 0 || currblock[(GRUB_DISK_SECTOR_SIZE << log_blocksize) / sizeof (*currblock) - 1] != grub_cpu_to_be32_compile_time (1)) continue; for (i = 0; i < (GRUB_DISK_SECTOR_SIZE << log_blocksize) / sizeof (*currblock); i++) checksum += grub_be_to_cpu32 (currblock[i]); if (checksum == 0) { data->log_blocksize = log_blocksize; data->disk = disk; data->htsize = grub_be_to_cpu32 (rblock->htsize); data->diropen.data = data; data->diropen.block = grub_be_to_cpu32 (data->bblock.rootblock); data->diropen.parent = NULL; grub_memcpy (&data->diropen.di, rootblock, sizeof (data->diropen.di)); grub_free (rootblock); return data; } } } fail: if (grub_errno == GRUB_ERR_NONE || grub_errno == GRUB_ERR_OUT_OF_RANGE) grub_error (GRUB_ERR_BAD_FS, "not an AFFS filesystem"); grub_free (data); grub_free (rootblock); return 0; } static char * grub_affs_read_symlink (grub_fshelp_node_t node) { struct grub_affs_data *data = node->data; grub_uint8_t *latin1, *utf8; const grub_size_t symlink_size = ((GRUB_DISK_SECTOR_SIZE << data->log_blocksize) - GRUB_AFFS_SYMLINK_OFFSET); latin1 = grub_malloc (symlink_size + 1); if (!latin1) return 0; grub_disk_read (data->disk, (grub_uint64_t) node->block << data->log_blocksize, GRUB_AFFS_SYMLINK_OFFSET, symlink_size, latin1); if (grub_errno) { grub_free (latin1); return 0; } latin1[symlink_size] = 0; utf8 = grub_calloc (GRUB_MAX_UTF8_PER_LATIN1 + 1, symlink_size); if (!utf8) { grub_free (latin1); return 0; } *grub_latin1_to_utf8 (utf8, latin1, symlink_size) = '\0'; grub_dprintf ("affs", "Symlink: `%s'\n", utf8); grub_free (latin1); if (utf8[0] == ':') utf8[0] = '/'; return (char *) utf8; } /* Helper for grub_affs_iterate_dir. */ static int grub_affs_create_node (grub_fshelp_node_t dir, grub_fshelp_iterate_dir_hook_t hook, void *hook_data, struct grub_fshelp_node **node, grub_uint32_t **hashtable, grub_uint32_t block, const struct grub_affs_file *fil) { struct grub_affs_data *data = dir->data; int type = GRUB_FSHELP_REG; grub_uint8_t name_u8[sizeof (fil->name) * GRUB_MAX_UTF8_PER_LATIN1 + 1]; grub_size_t len; unsigned int nest; *node = grub_zalloc (sizeof (**node)); if (!*node) { grub_free (*hashtable); return 1; } (*node)->data = data; (*node)->block = block; (*node)->parent = dir; len = fil->namelen; if (len > sizeof (fil->name)) len = sizeof (fil->name); *grub_latin1_to_utf8 (name_u8, fil->name, len) = '\0'; (*node)->di = *fil; for (nest = 0; nest < 8; nest++) { switch ((*node)->di.type) { case grub_cpu_to_be32_compile_time (GRUB_AFFS_FILETYPE_REG): type = GRUB_FSHELP_REG; break; case grub_cpu_to_be32_compile_time (GRUB_AFFS_FILETYPE_DIR): type = GRUB_FSHELP_DIR; break; case grub_cpu_to_be32_compile_time (GRUB_AFFS_FILETYPE_SYMLINK): type = GRUB_FSHELP_SYMLINK; break; case grub_cpu_to_be32_compile_time (GRUB_AFFS_FILETYPE_HARDLINK): { grub_err_t err; (*node)->block = grub_be_to_cpu32 ((*node)->di.hardlink); err = grub_disk_read (data->disk, (((grub_uint64_t) (*node)->block + 1) << data->log_blocksize) - 1, GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION, sizeof ((*node)->di), (char *) &(*node)->di); if (err) return 1; continue; } default: return 0; } break; } if (nest == 8) return 0; type |= GRUB_FSHELP_CASE_INSENSITIVE; if (hook ((char *) name_u8, type, *node, hook_data)) { grub_free (*hashtable); *node = 0; return 1; } *node = 0; return 0; } static int grub_affs_iterate_dir (grub_fshelp_node_t dir, grub_fshelp_iterate_dir_hook_t hook, void *hook_data) { unsigned int i; struct grub_affs_file file; struct grub_fshelp_node *node, *orig_node; struct grub_affs_data *data = dir->data; grub_uint32_t *hashtable; /* Create the directory entries for `.' and `..'. */ node = orig_node = grub_zalloc (sizeof (*node)); if (!node) return 1; *node = *dir; if (hook (".", GRUB_FSHELP_DIR, node, hook_data)) return 1; if (dir->parent) { *node = *dir->parent; if (hook ("..", GRUB_FSHELP_DIR, node, hook_data)) return 1; } hashtable = grub_calloc (data->htsize, sizeof (*hashtable)); if (!hashtable) return 1; grub_disk_read (data->disk, (grub_uint64_t) dir->block << data->log_blocksize, GRUB_AFFS_HASHTABLE_OFFSET, data->htsize * sizeof (*hashtable), (char *) hashtable); if (grub_errno) goto fail; for (i = 0; i < data->htsize; i++) { grub_uint32_t next; if (!hashtable[i]) continue; /* Every entry in the hashtable can be chained. Read the entire chain. */ next = grub_be_to_cpu32 (hashtable[i]); while (next) { grub_disk_read (data->disk, (((grub_uint64_t) next + 1) << data->log_blocksize) - 1, GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION, sizeof (file), (char *) &file); if (grub_errno) goto fail; if (grub_affs_create_node (dir, hook, hook_data, &node, &hashtable, next, &file)) { /* Node has been replaced in function. */ grub_free (orig_node); return 1; } next = grub_be_to_cpu32 (file.next); } } fail: grub_free (orig_node); grub_free (hashtable); return 0; } /* Open a file named NAME and initialize FILE. */ static grub_err_t grub_affs_open (struct grub_file *file, const char *name) { struct grub_affs_data *data; struct grub_fshelp_node *fdiro = 0; grub_dl_ref (my_mod); data = grub_affs_mount (file->device->disk); if (!data) goto fail; grub_fshelp_find_file (name, &data->diropen, &fdiro, grub_affs_iterate_dir, grub_affs_read_symlink, GRUB_FSHELP_REG); if (grub_errno) goto fail; file->size = grub_be_to_cpu32 (fdiro->di.size); data->diropen = *fdiro; grub_free (fdiro); file->data = data; file->offset = 0; return 0; fail: if (data && fdiro != &data->diropen) grub_free (fdiro); grub_free (data); grub_dl_unref (my_mod); return grub_errno; } static grub_err_t grub_affs_close (grub_file_t file) { struct grub_affs_data *data = (struct grub_affs_data *) file->data; grub_free (data->diropen.block_cache); grub_free (file->data); grub_dl_unref (my_mod); return GRUB_ERR_NONE; } /* Read LEN bytes data from FILE into BUF. */ static grub_ssize_t grub_affs_read (grub_file_t file, char *buf, grub_size_t len) { struct grub_affs_data *data = (struct grub_affs_data *) file->data; return grub_fshelp_read_file (data->diropen.data->disk, &data->diropen, file->read_hook, file->read_hook_data, file->offset, len, buf, grub_affs_read_block, grub_be_to_cpu32 (data->diropen.di.size), data->log_blocksize, 0); } static grub_int32_t aftime2ctime (const struct grub_affs_time *t) { return grub_be_to_cpu32 (t->day) * 86400 + grub_be_to_cpu32 (t->min) * 60 + grub_be_to_cpu32 (t->hz) / 50 + 8 * 365 * 86400 + 86400 * 2; } /* Context for grub_affs_dir. */ struct grub_affs_dir_ctx { grub_fs_dir_hook_t hook; void *hook_data; }; /* Helper for grub_affs_dir. */ static int grub_affs_dir_iter (const char *filename, enum grub_fshelp_filetype filetype, grub_fshelp_node_t node, void *data) { struct grub_affs_dir_ctx *ctx = data; struct grub_dirhook_info info; grub_memset (&info, 0, sizeof (info)); info.dir = ((filetype & GRUB_FSHELP_TYPE_MASK) == GRUB_FSHELP_DIR); info.mtimeset = 1; info.mtime = aftime2ctime (&node->di.mtime); grub_free (node); return ctx->hook (filename, &info, ctx->hook_data); } static grub_err_t grub_affs_dir (grub_device_t device, const char *path, grub_fs_dir_hook_t hook, void *hook_data) { struct grub_affs_dir_ctx ctx = { hook, hook_data }; struct grub_affs_data *data = 0; struct grub_fshelp_node *fdiro = 0; grub_dl_ref (my_mod); data = grub_affs_mount (device->disk); if (!data) goto fail; grub_fshelp_find_file (path, &data->diropen, &fdiro, grub_affs_iterate_dir, grub_affs_read_symlink, GRUB_FSHELP_DIR); if (grub_errno) goto fail; grub_affs_iterate_dir (fdiro, grub_affs_dir_iter, &ctx); fail: if (data && fdiro != &data->diropen) grub_free (fdiro); grub_free (data); grub_dl_unref (my_mod); return grub_errno; } static grub_err_t grub_affs_label (grub_device_t device, char **label) { struct grub_affs_data *data; struct grub_affs_file file; grub_disk_t disk = device->disk; grub_dl_ref (my_mod); data = grub_affs_mount (disk); if (data) { grub_size_t len; /* The rootblock maps quite well on a file header block, it's something we can use here. */ grub_disk_read (data->disk, (((grub_uint64_t) grub_be_to_cpu32 (data->bblock.rootblock) + 1) << data->log_blocksize) - 1, GRUB_DISK_SECTOR_SIZE - GRUB_AFFS_FILE_LOCATION, sizeof (file), &file); if (grub_errno) return grub_errno; len = file.namelen; if (len > sizeof (file.name)) len = sizeof (file.name); *label = grub_calloc (GRUB_MAX_UTF8_PER_LATIN1 + 1, len); if (*label) *grub_latin1_to_utf8 ((grub_uint8_t *) *label, file.name, len) = '\0'; } else *label = 0; grub_dl_unref (my_mod); grub_free (data); return grub_errno; } static grub_err_t grub_affs_mtime (grub_device_t device, grub_int64_t *t) { struct grub_affs_data *data; grub_disk_t disk = device->disk; struct grub_affs_time af_time; *t = 0; grub_dl_ref (my_mod); data = grub_affs_mount (disk); if (!data) { grub_dl_unref (my_mod); return grub_errno; } grub_disk_read (data->disk, (((grub_uint64_t) grub_be_to_cpu32 (data->bblock.rootblock) + 1) << data->log_blocksize) - 1, GRUB_DISK_SECTOR_SIZE - 40, sizeof (af_time), &af_time); if (grub_errno) { grub_dl_unref (my_mod); grub_free (data); return grub_errno; } *t = aftime2ctime (&af_time); grub_dl_unref (my_mod); grub_free (data); return GRUB_ERR_NONE; } static struct grub_fs grub_affs_fs = { .name = "affs", .fs_dir = grub_affs_dir, .fs_open = grub_affs_open, .fs_read = grub_affs_read, .fs_close = grub_affs_close, .fs_label = grub_affs_label, .fs_mtime = grub_affs_mtime, #ifdef GRUB_UTIL .reserved_first_sector = 0, .blocklist_install = 1, #endif .next = 0 }; GRUB_MOD_INIT(affs) { grub_fs_register (&grub_affs_fs); my_mod = mod; } GRUB_MOD_FINI(affs) { grub_fs_unregister (&grub_affs_fs); }