/* squash4.c - SquashFS */ /* * GRUB -- GRand Unified Bootloader * Copyright (C) 2010 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+"); /* object format Pointed by superblock RAW Fixed offset (0) data RAW ? Fixed offset (60) inode table Chunk superblock dir table Chunk superblock fragment table Chunk unk1 unk1 RAW, Chunk superblock unk2 RAW superblock UID/GID Chunk exttblptr exttblptr RAW superblock UID/GID table is the array ot uint32_t unk1 contains pointer to unk3 followed by some chunk. unk2 containts one uint64_t */ struct grub_squash_super { grub_uint32_t magic; #define SQUASH_MAGIC 0x73717368 grub_uint32_t dummy1; grub_uint32_t creation_time; grub_uint32_t dummy2; grub_uint64_t dummy3; grub_uint8_t flags; #define SQUASH_FLAG_UNCOMPRESSED_INODES 1 #define SQUASH_FLAG_UNCOMPRESSED_DATA 2 #define SQUASH_FLAG_UNCOMPRESSED_FRAGMENTS 8 grub_uint8_t dummy4[7]; grub_uint16_t root_ino_offset; grub_uint32_t root_ino_chunk; grub_uint16_t dummy5; grub_uint64_t total_size; grub_uint64_t exttbloffset; grub_uint64_t dummy6; grub_uint64_t inodeoffset; grub_uint64_t diroffset; grub_uint64_t unk1offset; grub_uint64_t unk2offset; } __attribute__ ((packed)); /* Chunk-based */ struct grub_squash_inode { /* Same values as direlem types. */ grub_uint16_t type; grub_uint16_t dummy[3]; grub_uint32_t mtime; union { struct { grub_uint32_t dummy; grub_uint32_t chunk; grub_uint32_t fragment; grub_uint32_t offset; grub_uint32_t size; } __attribute__ ((packed)) file; struct { grub_uint32_t dummy1; grub_uint32_t chunk; grub_uint32_t dummy2; grub_uint16_t size; grub_uint32_t offset; grub_uint16_t dummy3; } __attribute__ ((packed)) dir; struct { grub_uint64_t dummy; grub_uint32_t namelen; char name[0]; } __attribute__ ((packed)) symlink; } __attribute__ ((packed)); } __attribute__ ((packed)); /* Chunk-based. */ struct grub_squash_dirent_header { /* Actually the value is the number of elements - 1. */ grub_uint32_t nelems; grub_uint64_t ino_chunk; } __attribute__ ((packed)); struct grub_squash_dirent { grub_uint16_t ino_offset; grub_uint16_t dummy; grub_uint16_t type; #define SQUASH_TYPE_DIR 1 #define SQUASH_TYPE_REGULAR 2 #define SQUASH_TYPE_SYMLINK 3 /* Actually the value is the length of name - 1. */ grub_uint16_t namelen; char name[0]; } __attribute__ ((packed)); struct grub_squash_frag_desc { grub_uint64_t offset; grub_uint64_t dummy; } __attribute__ ((packed)); #define SQUASH_CHUNK_SIZE 0x2000 #define SQUASH_CHUNK_FLAGS 0x8000 #define SQUASH_CHUNK_UNCOMPRESSED 0x8000 struct grub_squash_data { grub_disk_t disk; struct grub_squash_super sb; struct grub_squash_inode ino; grub_uint64_t fragments; }; struct grub_fshelp_node { struct grub_squash_data *data; struct grub_squash_inode ino; grub_uint32_t ino_chunk; grub_uint16_t ino_offset; }; static grub_err_t read_chunk (grub_disk_t disk, void *buf, grub_size_t len, grub_uint64_t chunk, grub_off_t offset) { grub_uint64_t chunk_start; chunk_start = grub_le_to_cpu64 (chunk); while (len > 0) { grub_uint64_t csize; grub_uint16_t d; grub_err_t err; while (1) { err = grub_disk_read (disk, chunk_start >> GRUB_DISK_SECTOR_BITS, chunk_start & (GRUB_DISK_SECTOR_SIZE - 1), sizeof (d), &d); if (err) return err; if (offset < SQUASH_CHUNK_SIZE) break; offset -= SQUASH_CHUNK_SIZE; chunk_start += 2 + (grub_le_to_cpu16 (d) & ~SQUASH_CHUNK_FLAGS); } csize = SQUASH_CHUNK_SIZE - offset; if (csize > len) csize = len; if (grub_le_to_cpu16 (d) & SQUASH_CHUNK_UNCOMPRESSED) { grub_disk_addr_t a = chunk_start + 2 + offset; err = grub_disk_read (disk, (a >> GRUB_DISK_SECTOR_BITS), a & (GRUB_DISK_SECTOR_SIZE - 1), csize, buf); if (err) return err; } else { char *tmp; grub_size_t bsize = grub_le_to_cpu16 (d) & ~SQUASH_CHUNK_FLAGS; grub_disk_addr_t a = chunk_start + 2; tmp = grub_malloc (bsize); if (!tmp) return grub_errno; /* FIXME: buffer uncompressed data. */ err = grub_disk_read (disk, (a >> GRUB_DISK_SECTOR_BITS), a & (GRUB_DISK_SECTOR_SIZE - 1), bsize, tmp); if (err) { grub_free (tmp); return err; } if (grub_zlib_decompress (tmp, bsize, offset, buf, csize) < 0) { grub_free (tmp); return grub_errno; } grub_free (tmp); } len -= csize; offset += csize; buf = (char *) buf + csize; } return GRUB_ERR_NONE; } static struct grub_squash_data * squash_mount (grub_disk_t disk) { struct grub_squash_super sb; grub_err_t err; struct grub_squash_data *data; grub_uint64_t frag; err = grub_disk_read (disk, 0, 0, sizeof (sb), &sb); if (grub_errno == GRUB_ERR_OUT_OF_RANGE) grub_error (GRUB_ERR_BAD_FS, "not a squash4"); if (err) return NULL; if (grub_le_to_cpu32 (sb.magic) != SQUASH_MAGIC) { grub_error (GRUB_ERR_BAD_FS, "not squash4"); return NULL; } err = grub_disk_read (disk, grub_le_to_cpu32 (sb.unk1offset) >> GRUB_DISK_SECTOR_BITS, grub_le_to_cpu32 (sb.unk1offset) & (GRUB_DISK_SECTOR_SIZE - 1), sizeof (frag), &frag); if (grub_errno == GRUB_ERR_OUT_OF_RANGE) grub_error (GRUB_ERR_BAD_FS, "not a squash4"); if (err) return NULL; data = grub_malloc (sizeof (*data)); if (!data) return NULL; data->sb = sb; data->disk = disk; data->fragments = frag; return data; } static char * grub_squash_read_symlink (grub_fshelp_node_t node) { char *ret; grub_err_t err; ret = grub_malloc (grub_le_to_cpu32 (node->ino.symlink.namelen) + 1); err = read_chunk (node->data->disk, ret, grub_le_to_cpu32 (node->ino.symlink.namelen), grub_le_to_cpu64 (node->data->sb.inodeoffset) + node->ino_chunk, node->ino_offset + (node->ino.symlink.name - (char *) &node->ino)); if (err) { grub_free (ret); return NULL; } ret[grub_le_to_cpu32 (node->ino.symlink.namelen)] = 0; return ret; } static int grub_squash_iterate_dir (grub_fshelp_node_t dir, int NESTED_FUNC_ATTR (*hook) (const char *filename, enum grub_fshelp_filetype filetype, grub_fshelp_node_t node)) { grub_uint32_t off = grub_le_to_cpu16 (dir->ino.dir.offset); grub_uint32_t endoff; unsigned i; /* FIXME: why - 3 ? */ endoff = grub_le_to_cpu32 (dir->ino.dir.size) + off - 3; while (off < endoff) { struct grub_squash_dirent_header dh; grub_err_t err; err = read_chunk (dir->data->disk, &dh, sizeof (dh), grub_le_to_cpu64 (dir->data->sb.diroffset) + grub_le_to_cpu32 (dir->ino.dir.chunk), off); if (err) return 0; off += sizeof (dh); for (i = 0; i < (unsigned) grub_le_to_cpu16 (dh.nelems) + 1; i++) { char *buf; int r; struct grub_fshelp_node *node; enum grub_fshelp_filetype filetype = GRUB_FSHELP_REG; struct grub_squash_dirent di; struct grub_squash_inode ino; err = read_chunk (dir->data->disk, &di, sizeof (di), grub_le_to_cpu64 (dir->data->sb.diroffset) + grub_le_to_cpu32 (dir->ino.dir.chunk), off); if (err) return 0; off += sizeof (di); err = read_chunk (dir->data->disk, &ino, sizeof (ino), grub_le_to_cpu64 (dir->data->sb.inodeoffset) + grub_le_to_cpu32 (dh.ino_chunk), grub_cpu_to_le16 (di.ino_offset)); if (err) return 0; buf = grub_malloc (grub_le_to_cpu16 (di.namelen) + 2); if (!buf) return 0; err = read_chunk (dir->data->disk, buf, grub_le_to_cpu16 (di.namelen) + 1, grub_le_to_cpu64 (dir->data->sb.diroffset) + grub_le_to_cpu32 (dir->ino.dir.chunk), off); if (err) return 0; off += grub_le_to_cpu16 (di.namelen) + 1; buf[grub_le_to_cpu16 (di.namelen) + 1] = 0; if (grub_le_to_cpu16 (di.type) == SQUASH_TYPE_DIR) filetype = GRUB_FSHELP_DIR; if (grub_le_to_cpu16 (di.type) == SQUASH_TYPE_SYMLINK) filetype = GRUB_FSHELP_SYMLINK; node = grub_malloc (sizeof (*node)); if (! node) return 0; *node = *dir; node->ino = ino; node->ino_chunk = grub_le_to_cpu32 (dh.ino_chunk); node->ino_offset = grub_le_to_cpu16 (di.ino_offset); r = hook (buf, filetype, node); grub_free (buf); if (r) return r; } } return 0; } static grub_err_t make_root_node (struct grub_squash_data *data, struct grub_fshelp_node *root) { grub_memset (root, 0, sizeof (*root)); root->data = data; return read_chunk (data->disk, &root->ino, sizeof (root->ino), grub_le_to_cpu64 (data->sb.inodeoffset) + grub_le_to_cpu16 (data->sb.root_ino_chunk), grub_cpu_to_le16 (data->sb.root_ino_offset)); } static grub_err_t grub_squash_dir (grub_device_t device, const char *path, int (*hook) (const char *filename, const struct grub_dirhook_info *info)) { auto int NESTED_FUNC_ATTR iterate (const char *filename, enum grub_fshelp_filetype filetype, grub_fshelp_node_t node); int NESTED_FUNC_ATTR iterate (const char *filename, enum grub_fshelp_filetype filetype, grub_fshelp_node_t node) { 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 = grub_le_to_cpu32 (node->ino.mtime); return hook (filename, &info); } struct grub_squash_data *data = 0; struct grub_fshelp_node *fdiro = 0; struct grub_fshelp_node root; grub_err_t err; data = squash_mount (device->disk); if (! data) return grub_errno; err = make_root_node (data, &root); if (err) return err; grub_fshelp_find_file (path, &root, &fdiro, grub_squash_iterate_dir, grub_squash_read_symlink, GRUB_FSHELP_DIR); if (!grub_errno) grub_squash_iterate_dir (fdiro, iterate); grub_free (data); return grub_errno; } static grub_err_t grub_squash_open (struct grub_file *file, const char *name) { struct grub_squash_data *data = 0; struct grub_fshelp_node *fdiro = 0; struct grub_fshelp_node root; grub_err_t err; data = squash_mount (file->device->disk); if (! data) return grub_errno; err = make_root_node (data, &root); if (err) return err; grub_fshelp_find_file (name, &root, &fdiro, grub_squash_iterate_dir, grub_squash_read_symlink, GRUB_FSHELP_REG); if (grub_errno) { grub_free (data); return grub_errno; } file->data = data; data->ino = fdiro->ino; file->size = grub_le_to_cpu32 (fdiro->ino.file.size); return GRUB_ERR_NONE; } static grub_ssize_t grub_squash_read_data (struct grub_squash_data *data, grub_disk_t disk, const struct grub_squash_inode *ino, grub_off_t off, char *buf, grub_size_t len) { grub_err_t err; grub_uint64_t a, b; int compressed = 0; if (grub_le_to_cpu16 (ino->file.fragment) == 0xffff) { if (grub_le_to_cpu32 (ino->file.chunk)) a = grub_le_to_cpu32 (ino->file.chunk); else a = sizeof (struct grub_squash_super); compressed = !(data->sb.flags & SQUASH_FLAG_UNCOMPRESSED_DATA); } else { struct grub_squash_frag_desc frag; err = read_chunk (disk, &frag, sizeof (frag), data->fragments, sizeof (frag) * grub_le_to_cpu16 (ino->file.fragment)); if (err) return -1; a = grub_le_to_cpu64 (frag.offset) + grub_le_to_cpu32 (ino->file.chunk); compressed = !(data->sb.flags & SQUASH_FLAG_UNCOMPRESSED_FRAGMENTS); } b = grub_le_to_cpu32 (data->ino.file.offset) + off; /* FIXME: cache uncompressed chunks. */ if (compressed) err = grub_zlib_disk_read (disk, a, b, buf, len); else err = grub_disk_read (disk, (a + b) >> GRUB_DISK_SECTOR_BITS, (a + b) & (GRUB_DISK_SECTOR_SIZE - 1), len, buf); if (err) return -1; return len; } static grub_ssize_t grub_squash_read (grub_file_t file, char *buf, grub_size_t len) { struct grub_squash_data *data = file->data; return grub_squash_read_data (data, file->device->disk, &data->ino, file->offset, buf, len); } static grub_err_t grub_squash_close (grub_file_t file) { grub_free (file->data); return GRUB_ERR_NONE; } static grub_err_t grub_squash_mtime (grub_device_t dev, grub_int32_t *tm) { struct grub_squash_data *data = 0; data = squash_mount (dev->disk); if (! data) return grub_errno; *tm = grub_le_to_cpu32 (data->sb.creation_time); grub_free (data); return GRUB_ERR_NONE; } static struct grub_fs grub_squash_fs = { .name = "squash4", .dir = grub_squash_dir, .open = grub_squash_open, .read = grub_squash_read, .close = grub_squash_close, .mtime = grub_squash_mtime, #ifdef GRUB_UTIL .reserved_first_sector = 0, #endif .next = 0 }; GRUB_MOD_INIT(squash4) { grub_fs_register (&grub_squash_fs); } GRUB_MOD_FINI(squash4) { grub_fs_unregister (&grub_squash_fs); }