diff --git a/INSTALL b/INSTALL index 8d9207c84..84030c9f4 100644 --- a/INSTALL +++ b/INSTALL @@ -77,15 +77,15 @@ Prerequisites for make-check: * If running a Linux kernel the following modules must be loaded: - fuse, loop - - btrfs, ext4, f2fs, fat, hfs, hfsplus, jfs, mac-roman, minix, nilfs2, + - btrfs, erofs, ext4, f2fs, fat, hfs, hfsplus, jfs, mac-roman, minix, nilfs2, reiserfs, udf, xfs - On newer kernels, the exfat kernel modules may be used instead of the exfat FUSE filesystem * The following are Debian named packages required mostly for the full suite of filesystem testing (but some are needed by other tests as well): - - btrfs-progs, dosfstools, e2fsprogs, exfat-utils, f2fs-tools, genromfs, - hfsprogs, jfsutils, nilfs-tools, ntfs-3g, reiserfsprogs, squashfs-tools, - reiserfsprogs, udftools, xfsprogs, zfs-fuse + - btrfs-progs, dosfstools, e2fsprogs, erofs-utils, exfat-utils, f2fs-tools, + genromfs, hfsprogs, jfsutils, nilfs-tools, ntfs-3g, reiserfsprogs, + squashfs-tools, reiserfsprogs, udftools, xfsprogs, zfs-fuse - exfat-fuse, if not using the exfat kernel module - gzip, lzop, xz-utils - attr, cpio, g++, gawk, parted, recode, tar, util-linux diff --git a/Makefile.util.def b/Makefile.util.def index 9432365a9..8d3bc107f 100644 --- a/Makefile.util.def +++ b/Makefile.util.def @@ -98,6 +98,7 @@ library = { common = grub-core/fs/cpio_be.c; common = grub-core/fs/odc.c; common = grub-core/fs/newc.c; + common = grub-core/fs/erofs.c; common = grub-core/fs/ext2.c; common = grub-core/fs/fat.c; common = grub-core/fs/exfat.c; diff --git a/docs/grub.texi b/docs/grub.texi index 1f6d0c724..cb3a14fca 100644 --- a/docs/grub.texi +++ b/docs/grub.texi @@ -353,6 +353,7 @@ blocklist notation. The currently supported filesystem types are @dfn{Amiga Fast FileSystem (AFFS)}, @dfn{AtheOS fs}, @dfn{BeFS}, @dfn{BtrFS} (including raid0, raid1, raid10, gzip and lzo), @dfn{cpio} (little- and big-endian bin, odc and newc variants), +@dfn{EROFS} (only uncompressed support for now), @dfn{Linux ext2/ext3/ext4}, @dfn{DOS FAT12/FAT16/FAT32}, @dfn{exFAT}, @dfn{F2FS}, @dfn{HFS}, @dfn{HFS+}, @dfn{ISO9660} (including Joliet, Rock-ridge and multi-chunk files), @@ -6276,7 +6277,7 @@ assumed to be encoded in UTF-8. NTFS, JFS, UDF, HFS+, exFAT, long filenames in FAT, Joliet part of ISO9660 are treated as UTF-16 as per specification. AFS and BFS are read as UTF-8, again according to specification. BtrFS, cpio, tar, squash4, minix, -minix2, minix3, ROMFS, ReiserFS, XFS, ext2, ext3, ext4, FAT (short names), +minix2, minix3, ROMFS, ReiserFS, XFS, EROFS, ext2, ext3, ext4, FAT (short names), F2FS, RockRidge part of ISO9660, nilfs2, UFS1, UFS2 and ZFS are assumed to be UTF-8. This might be false on systems configured with legacy charset but as long as the charset used is superset of ASCII you should be able to diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def index 8e1b1d9f3..7fa9446bd 100644 --- a/grub-core/Makefile.core.def +++ b/grub-core/Makefile.core.def @@ -1442,6 +1442,11 @@ module = { common = fs/odc.c; }; +module = { + name = erofs; + common = fs/erofs.c; +}; + module = { name = ext2; common = fs/ext2.c; diff --git a/grub-core/fs/erofs.c b/grub-core/fs/erofs.c new file mode 100644 index 000000000..46cfc2e5c --- /dev/null +++ b/grub-core/fs/erofs.c @@ -0,0 +1,1000 @@ +/* erofs.c - Enhanced Read-Only File System */ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2024 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 +#include + +GRUB_MOD_LICENSE ("GPLv3+"); + +#define EROFS_SUPER_OFFSET 1024 +#define EROFS_MAGIC 0xE0F5E1E2 +#define EROFS_ISLOTBITS 5 + +#define EROFS_FEATURE_INCOMPAT_CHUNKED_FILE 0x00000004 +#define EROFS_ALL_FEATURE_INCOMPAT EROFS_FEATURE_INCOMPAT_CHUNKED_FILE + +struct grub_erofs_super +{ + grub_uint32_t magic; + grub_uint32_t checksum; + grub_uint32_t feature_compat; + grub_uint8_t log2_blksz; + grub_uint8_t sb_extslots; + + grub_uint16_t root_nid; + grub_uint64_t inos; + + grub_uint64_t build_time; + grub_uint32_t build_time_nsec; + grub_uint32_t blocks; + grub_uint32_t meta_blkaddr; + grub_uint32_t xattr_blkaddr; + grub_packed_guid_t uuid; + grub_uint8_t volume_name[16]; + grub_uint32_t feature_incompat; + + union + { + grub_uint16_t available_compr_algs; + grub_uint16_t lz4_max_distance; + } GRUB_PACKED u1; + + grub_uint16_t extra_devices; + grub_uint16_t devt_slotoff; + grub_uint8_t log2_dirblksz; + grub_uint8_t xattr_prefix_count; + grub_uint32_t xattr_prefix_start; + grub_uint64_t packed_nid; + grub_uint8_t reserved2[24]; +} GRUB_PACKED; + +#define EROFS_INODE_LAYOUT_COMPACT 0 +#define EROFS_INODE_LAYOUT_EXTENDED 1 + +#define EROFS_INODE_FLAT_PLAIN 0 +#define EROFS_INODE_COMPRESSED_FULL 1 +#define EROFS_INODE_FLAT_INLINE 2 +#define EROFS_INODE_COMPRESSED_COMPACT 3 +#define EROFS_INODE_CHUNK_BASED 4 + +#define EROFS_I_VERSION_MASKS 0x01 +#define EROFS_I_DATALAYOUT_MASKS 0x07 + +#define EROFS_I_VERSION_BIT 0 +#define EROFS_I_DATALAYOUT_BIT 1 + +struct grub_erofs_inode_chunk_info +{ + grub_uint16_t format; + grub_uint16_t reserved; +} GRUB_PACKED; + +#define EROFS_CHUNK_FORMAT_BLKBITS_MASK 0x001F +#define EROFS_CHUNK_FORMAT_INDEXES 0x0020 + +#define EROFS_BLOCK_MAP_ENTRY_SIZE 4 +#define EROFS_MAP_MAPPED 0x02 + +#define EROFS_NULL_ADDR 1 +#define EROFS_NAME_LEN 255 +#define EROFS_PATH_LEN 4096 +#define EROFS_MIN_LOG2_BLOCK_SIZE 9 +#define EROFS_MAX_LOG2_BLOCK_SIZE 16 + +struct grub_erofs_inode_chunk_index +{ + grub_uint16_t advise; + grub_uint16_t device_id; + grub_uint32_t blkaddr; +}; + +union grub_erofs_inode_i_u +{ + grub_uint32_t compressed_blocks; + grub_uint32_t raw_blkaddr; + + grub_uint32_t rdev; + + struct grub_erofs_inode_chunk_info c; +}; + +struct grub_erofs_inode_compact +{ + grub_uint16_t i_format; + + grub_uint16_t i_xattr_icount; + grub_uint16_t i_mode; + grub_uint16_t i_nlink; + grub_uint32_t i_size; + grub_uint32_t i_reserved; + + union grub_erofs_inode_i_u i_u; + + grub_uint32_t i_ino; + grub_uint16_t i_uid; + grub_uint16_t i_gid; + grub_uint32_t i_reserved2; +} GRUB_PACKED; + +struct grub_erofs_inode_extended +{ + grub_uint16_t i_format; + + grub_uint16_t i_xattr_icount; + grub_uint16_t i_mode; + grub_uint16_t i_reserved; + grub_uint64_t i_size; + + union grub_erofs_inode_i_u i_u; + + grub_uint32_t i_ino; + + grub_uint32_t i_uid; + grub_uint32_t i_gid; + grub_uint64_t i_mtime; + grub_uint32_t i_mtime_nsec; + grub_uint32_t i_nlink; + grub_uint8_t i_reserved2[16]; +} GRUB_PACKED; + +union grub_erofs_inode +{ + struct grub_erofs_inode_compact c; + struct grub_erofs_inode_extended e; +} GRUB_PACKED; + +#define EROFS_FT_UNKNOWN 0 +#define EROFS_FT_REG_FILE 1 +#define EROFS_FT_DIR 2 +#define EROFS_FT_CHRDEV 3 +#define EROFS_FT_BLKDEV 4 +#define EROFS_FT_FIFO 5 +#define EROFS_FT_SOCK 6 +#define EROFS_FT_SYMLINK 7 + +struct grub_erofs_dirent +{ + grub_uint64_t nid; + grub_uint16_t nameoff; + grub_uint8_t file_type; + grub_uint8_t reserved; +} GRUB_PACKED; + +struct grub_erofs_map_blocks +{ + grub_uint64_t m_pa; /* physical address */ + grub_uint64_t m_la; /* logical address */ + grub_uint64_t m_plen; /* physical length */ + grub_uint64_t m_llen; /* logical length */ + grub_uint32_t m_flags; +}; + +struct grub_erofs_xattr_ibody_header +{ + grub_uint32_t h_reserved; + grub_uint8_t h_shared_count; + grub_uint8_t h_reserved2[7]; + grub_uint32_t h_shared_xattrs[0]; +}; + +struct grub_fshelp_node +{ + struct grub_erofs_data *data; + union grub_erofs_inode inode; + + grub_uint64_t ino; + grub_uint8_t inode_type; + grub_uint8_t inode_datalayout; + + /* If the inode has been read into memory? */ + bool inode_loaded; +}; + +struct grub_erofs_data +{ + grub_disk_t disk; + struct grub_erofs_super sb; + + struct grub_fshelp_node inode; +}; + +#define erofs_blocksz(data) (((grub_uint32_t) 1) << data->sb.log2_blksz) + +static grub_size_t +grub_erofs_strnlen (const char *s, grub_size_t n) +{ + const char *p = s; + + if (n == 0) + return 0; + + while (n-- && *p) + p++; + + return p - s; +} + +static grub_uint64_t +erofs_iloc (grub_fshelp_node_t node) +{ + struct grub_erofs_super *sb = &node->data->sb; + + return ((grub_uint64_t) grub_le_to_cpu32 (sb->meta_blkaddr) << sb->log2_blksz) + + (node->ino << EROFS_ISLOTBITS); +} + +static grub_err_t +erofs_read_inode (struct grub_erofs_data *data, grub_fshelp_node_t node) +{ + union grub_erofs_inode *di; + grub_err_t err; + grub_uint16_t i_format; + grub_uint64_t addr = erofs_iloc (node); + + di = (union grub_erofs_inode *) &node->inode; + + err = grub_disk_read (data->disk, addr >> GRUB_DISK_SECTOR_BITS, + addr & (GRUB_DISK_SECTOR_SIZE - 1), + sizeof (struct grub_erofs_inode_compact), &di->c); + if (err != GRUB_ERR_NONE) + return err; + + i_format = grub_le_to_cpu16 (di->c.i_format); + node->inode_type = (i_format >> EROFS_I_VERSION_BIT) & EROFS_I_VERSION_MASKS; + node->inode_datalayout = (i_format >> EROFS_I_DATALAYOUT_BIT) & EROFS_I_DATALAYOUT_MASKS; + + switch (node->inode_type) + { + case EROFS_INODE_LAYOUT_EXTENDED: + addr += sizeof (struct grub_erofs_inode_compact); + err = grub_disk_read (data->disk, addr >> GRUB_DISK_SECTOR_BITS, + addr & (GRUB_DISK_SECTOR_SIZE - 1), + sizeof (struct grub_erofs_inode_extended) - sizeof (struct grub_erofs_inode_compact), + (grub_uint8_t *) di + sizeof (struct grub_erofs_inode_compact)); + if (err != GRUB_ERR_NONE) + return err; + break; + case EROFS_INODE_LAYOUT_COMPACT: + break; + default: + return grub_error (GRUB_ERR_BAD_FS, "invalid type %u @ inode %" PRIuGRUB_UINT64_T, + node->inode_type, node->ino); + } + + node->inode_loaded = true; + + return 0; +} + +static grub_uint64_t +erofs_inode_size (grub_fshelp_node_t node) +{ + return node->inode_type == EROFS_INODE_LAYOUT_COMPACT + ? sizeof (struct grub_erofs_inode_compact) + : sizeof (struct grub_erofs_inode_extended); +} + +static grub_uint64_t +erofs_inode_file_size (grub_fshelp_node_t node) +{ + union grub_erofs_inode *di = (union grub_erofs_inode *) &node->inode; + + return node->inode_type == EROFS_INODE_LAYOUT_COMPACT + ? grub_le_to_cpu32 (di->c.i_size) + : grub_le_to_cpu64 (di->e.i_size); +} + +static grub_uint32_t +erofs_inode_xattr_ibody_size (grub_fshelp_node_t node) +{ + grub_uint16_t cnt = grub_le_to_cpu16 (node->inode.e.i_xattr_icount); + + if (cnt == 0) + return 0; + + return sizeof (struct grub_erofs_xattr_ibody_header) + ((cnt - 1) * sizeof (grub_uint32_t)); +} + +static grub_uint64_t +erofs_inode_mtime (grub_fshelp_node_t node) +{ + return node->inode_type == EROFS_INODE_LAYOUT_COMPACT + ? grub_le_to_cpu64 (node->data->sb.build_time) + : grub_le_to_cpu64 (node->inode.e.i_mtime); +} + +static grub_err_t +erofs_map_blocks_flatmode (grub_fshelp_node_t node, + struct grub_erofs_map_blocks *map) +{ + grub_uint64_t nblocks, lastblk, file_size; + bool tailendpacking = (node->inode_datalayout == EROFS_INODE_FLAT_INLINE); + grub_uint64_t blocksz = erofs_blocksz (node->data); + + /* `file_size` is checked by caller and cannot be zero, hence nblocks > 0. */ + file_size = erofs_inode_file_size (node); + if (grub_add (file_size, blocksz - 1, &nblocks)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "nblocks overflow"); + nblocks >>= node->data->sb.log2_blksz; + lastblk = nblocks - tailendpacking; + + map->m_flags = EROFS_MAP_MAPPED; + + /* No overflow as (lastblk <= nblocks) && (nblocks * blocksz <= UINT64_MAX - blocksz + 1). */ + if (map->m_la < (lastblk * blocksz)) + { + if (grub_mul ((grub_uint64_t) grub_le_to_cpu32 (node->inode.e.i_u.raw_blkaddr), blocksz, &map->m_pa) || + grub_add (map->m_pa, map->m_la, &map->m_pa)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "m_pa overflow"); + if (grub_sub (lastblk * blocksz, map->m_la, &map->m_plen)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "m_plen underflow"); + } + else if (tailendpacking) + { + if (grub_add (erofs_iloc (node), erofs_inode_size (node), &map->m_pa) || + grub_add (map->m_pa, erofs_inode_xattr_ibody_size (node), &map->m_pa) || + grub_add (map->m_pa, map->m_la % blocksz, &map->m_pa)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "m_pa overflow when handling tailpacking"); + if (grub_sub (file_size, map->m_la, &map->m_plen)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "m_plen overflow when handling tailpacking"); + + /* No overflow as map->m_plen <= UINT64_MAX - blocksz + 1. */ + if (((map->m_pa % blocksz) + map->m_plen) > blocksz) + return grub_error (GRUB_ERR_BAD_FS, + "inline data cross block boundary @ inode %" PRIuGRUB_UINT64_T, + node->ino); + } + else + return grub_error (GRUB_ERR_BAD_FS, + "invalid map->m_la=%" PRIuGRUB_UINT64_T + " @ inode %" PRIuGRUB_UINT64_T, + map->m_la, node->ino); + + map->m_llen = map->m_plen; + return GRUB_ERR_NONE; +} + +static grub_err_t +erofs_map_blocks_chunkmode (grub_fshelp_node_t node, + struct grub_erofs_map_blocks *map) +{ + grub_uint16_t chunk_format = grub_le_to_cpu16 (node->inode.e.i_u.c.format); + grub_uint64_t unit, pos, chunknr, blkaddr; + grub_uint8_t chunkbits; + grub_err_t err; + + if (chunk_format & EROFS_CHUNK_FORMAT_INDEXES) + unit = sizeof (struct grub_erofs_inode_chunk_index); + else + unit = EROFS_BLOCK_MAP_ENTRY_SIZE; + + chunkbits = node->data->sb.log2_blksz + (chunk_format & EROFS_CHUNK_FORMAT_BLKBITS_MASK); + if (chunkbits > 63) + return grub_error (GRUB_ERR_BAD_FS, "invalid chunkbits %u @ inode %" PRIuGRUB_UINT64_T, + chunkbits, node->ino); + + chunknr = map->m_la >> chunkbits; + + if (grub_add (erofs_iloc (node), erofs_inode_size (node), &pos)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "chunkmap position overflow when adding inode size"); + + if (grub_add (pos, erofs_inode_xattr_ibody_size (node), &pos)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "chunkmap position overflow when adding xattr size"); + + if (ALIGN_UP_OVF (pos, unit, &pos)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "position overflow when seeking at the start of chunkmap"); + + /* No overflow for multiplication as chunkbits >= 9 and sizeof(unit) <= 8. */ + if (grub_add (pos, chunknr * unit, &pos)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "chunkmap position overflow when finding the specific chunk"); + + map->m_la = chunknr << chunkbits; + + if (grub_sub (erofs_inode_file_size (node), map->m_la, &map->m_plen)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "m_plen underflow"); + map->m_plen = grub_min (((grub_uint64_t) 1) << chunkbits, + ALIGN_UP (map->m_plen, erofs_blocksz (node->data))); + + if (chunk_format & EROFS_CHUNK_FORMAT_INDEXES) + { + struct grub_erofs_inode_chunk_index idx; + + err = grub_disk_read (node->data->disk, pos >> GRUB_DISK_SECTOR_BITS, + pos & (GRUB_DISK_SECTOR_SIZE - 1), unit, &idx); + if (err != GRUB_ERR_NONE) + return err; + + blkaddr = grub_le_to_cpu32 (idx.blkaddr); + } + else + { + grub_uint32_t blkaddr_le; + + err = grub_disk_read (node->data->disk, pos >> GRUB_DISK_SECTOR_BITS, + pos & (GRUB_DISK_SECTOR_SIZE - 1), unit, &blkaddr_le); + if (err != GRUB_ERR_NONE) + return err; + + blkaddr = grub_le_to_cpu32 (blkaddr_le); + } + + if (blkaddr == EROFS_NULL_ADDR) + { + map->m_pa = 0; + map->m_flags = 0; + } + else + { + map->m_pa = blkaddr << node->data->sb.log2_blksz; + map->m_flags = EROFS_MAP_MAPPED; + } + + map->m_llen = map->m_plen; + return GRUB_ERR_NONE; +} + +static grub_err_t +erofs_map_blocks (grub_fshelp_node_t node, struct grub_erofs_map_blocks *map) +{ + if (map->m_la >= erofs_inode_file_size (node)) + { + map->m_llen = map->m_plen = 0; + map->m_pa = 0; + map->m_flags = 0; + return GRUB_ERR_NONE; + } + + if (node->inode_datalayout != EROFS_INODE_CHUNK_BASED) + return erofs_map_blocks_flatmode (node, map); + else + return erofs_map_blocks_chunkmode (node, map); +} + +static grub_err_t +erofs_read_raw_data (grub_fshelp_node_t node, grub_uint8_t *buf, grub_uint64_t size, + grub_uint64_t offset, grub_uint64_t *bytes) +{ + struct grub_erofs_map_blocks map = {0}; + grub_uint64_t cur; + grub_err_t err; + + if (bytes) + *bytes = 0; + + if (node->inode_loaded == false) + { + err = erofs_read_inode (node->data, node); + if (err != GRUB_ERR_NONE) + return err; + } + + cur = offset; + while (cur < offset + size) + { + grub_uint8_t *const estart = buf + cur - offset; + grub_uint64_t eend, moff = 0; + + map.m_la = cur; + err = erofs_map_blocks (node, &map); + if (err != GRUB_ERR_NONE) + return err; + + if (grub_add(map.m_la, map.m_llen, &eend)) + return grub_error (GRUB_ERR_OUT_OF_RANGE, "eend overflow"); + + eend = grub_min (eend, offset + size); + if (!(map.m_flags & EROFS_MAP_MAPPED)) + { + if (!map.m_llen) + { + /* Reached EOF. */ + grub_memset (estart, 0, offset + size - cur); + cur = offset + size; + continue; + } + + /* It's a hole. */ + grub_memset (estart, 0, eend - cur); + if (bytes) + *bytes += eend - cur; + cur = eend; + continue; + } + + if (cur > map.m_la) + { + moff = cur - map.m_la; + map.m_la = cur; + } + + err = grub_disk_read (node->data->disk, + (map.m_pa + moff) >> GRUB_DISK_SECTOR_BITS, + (map.m_pa + moff) & (GRUB_DISK_SECTOR_SIZE - 1), + eend - map.m_la, estart); + if (err != GRUB_ERR_NONE) + return err; + + if (bytes) + *bytes += eend - map.m_la; + + cur = eend; + } + + return GRUB_ERR_NONE; +} + +static int +erofs_iterate_dir (grub_fshelp_node_t dir, grub_fshelp_iterate_dir_hook_t hook, + void *hook_data) +{ + grub_uint64_t offset = 0, file_size; + grub_uint32_t blocksz = erofs_blocksz (dir->data); + grub_uint8_t *buf; + grub_err_t err; + + if (dir->inode_loaded == false) + { + err = erofs_read_inode (dir->data, dir); + if (err != GRUB_ERR_NONE) + return 0; + } + + file_size = erofs_inode_file_size (dir); + buf = grub_malloc (blocksz); + if (buf == NULL) + return 0; + + while (offset < file_size) + { + grub_uint64_t maxsize = grub_min (blocksz, file_size - offset); + struct grub_erofs_dirent *de = (void *) buf, *end; + grub_uint16_t nameoff; + + err = erofs_read_raw_data (dir, buf, maxsize, offset, NULL); + if (err != GRUB_ERR_NONE) + goto not_found; + + nameoff = grub_le_to_cpu16 (de->nameoff); + if (nameoff < sizeof (struct grub_erofs_dirent) || nameoff >= maxsize) + { + grub_error (GRUB_ERR_BAD_FS, + "invalid nameoff %u @ inode %" PRIuGRUB_UINT64_T, + nameoff, dir->ino); + goto not_found; + } + + end = (struct grub_erofs_dirent *) ((grub_uint8_t *) de + nameoff); + while (de < end) + { + struct grub_fshelp_node *fdiro; + enum grub_fshelp_filetype type; + char filename[EROFS_NAME_LEN + 1]; + grub_size_t de_namelen; + const char *de_name; + + fdiro = grub_malloc (sizeof (struct grub_fshelp_node)); + if (fdiro == NULL) + goto not_found; + + fdiro->data = dir->data; + fdiro->ino = grub_le_to_cpu64 (de->nid); + fdiro->inode_loaded = false; + + nameoff = grub_le_to_cpu16 (de->nameoff); + if (nameoff < sizeof (struct grub_erofs_dirent) || nameoff >= maxsize) + { + grub_error (GRUB_ERR_BAD_FS, + "invalid nameoff %u @ inode %" PRIuGRUB_UINT64_T, + nameoff, dir->ino); + grub_free (fdiro); + goto not_found; + } + + de_name = (char *) buf + nameoff; + if (de + 1 >= end) + de_namelen = grub_erofs_strnlen (de_name, maxsize - nameoff); + else + { + if (grub_sub (grub_le_to_cpu16 (de[1].nameoff), nameoff, &de_namelen)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "de_namelen underflow"); + grub_free (fdiro); + goto not_found; + } + } + + if (nameoff + de_namelen > maxsize || de_namelen > EROFS_NAME_LEN) + { + grub_error (GRUB_ERR_BAD_FS, + "invalid de_namelen %" PRIuGRUB_SIZE + " @ inode %" PRIuGRUB_UINT64_T, + de_namelen, dir->ino); + grub_free (fdiro); + goto not_found; + } + + grub_memcpy (filename, de_name, de_namelen); + filename[de_namelen] = '\0'; + + switch (grub_le_to_cpu16 (de->file_type)) + { + case EROFS_FT_REG_FILE: + case EROFS_FT_BLKDEV: + case EROFS_FT_CHRDEV: + case EROFS_FT_FIFO: + case EROFS_FT_SOCK: + type = GRUB_FSHELP_REG; + break; + case EROFS_FT_DIR: + type = GRUB_FSHELP_DIR; + break; + case EROFS_FT_SYMLINK: + type = GRUB_FSHELP_SYMLINK; + break; + case EROFS_FT_UNKNOWN: + default: + type = GRUB_FSHELP_UNKNOWN; + } + + if (hook (filename, type, fdiro, hook_data)) + { + grub_free (buf); + return 1; + } + + ++de; + } + + offset += maxsize; + } + + not_found: + grub_free (buf); + return 0; +} + +static char * +erofs_read_symlink (grub_fshelp_node_t node) +{ + char *symlink; + grub_size_t sz; + grub_err_t err; + + if (node->inode_loaded == false) + { + err = erofs_read_inode (node->data, node); + if (err != GRUB_ERR_NONE) + return NULL; + } + + sz = erofs_inode_file_size (node); + if (sz >= EROFS_PATH_LEN) + { + grub_error (GRUB_ERR_BAD_FS, + "symlink too long @ inode %" PRIuGRUB_UINT64_T, node->ino); + return NULL; + } + + symlink = grub_malloc (sz + 1); + if (symlink == NULL) + return NULL; + + err = erofs_read_raw_data (node, (grub_uint8_t *) symlink, sz, 0, NULL); + if (err != GRUB_ERR_NONE) + { + grub_free (symlink); + return NULL; + } + + symlink[sz] = '\0'; + return symlink; +} + +static struct grub_erofs_data * +erofs_mount (grub_disk_t disk, bool read_root) +{ + struct grub_erofs_super sb; + grub_err_t err; + struct grub_erofs_data *data; + grub_uint32_t feature; + + err = grub_disk_read (disk, EROFS_SUPER_OFFSET >> GRUB_DISK_SECTOR_BITS, 0, + sizeof (sb), &sb); + if (err != GRUB_ERR_NONE) + return NULL; + if (sb.magic != grub_cpu_to_le32_compile_time (EROFS_MAGIC) || + grub_le_to_cpu32 (sb.log2_blksz) < EROFS_MIN_LOG2_BLOCK_SIZE || + grub_le_to_cpu32 (sb.log2_blksz) > EROFS_MAX_LOG2_BLOCK_SIZE) + { + grub_error (GRUB_ERR_BAD_FS, "not a valid erofs filesystem"); + return NULL; + } + + feature = grub_le_to_cpu32 (sb.feature_incompat); + if (feature & ~EROFS_ALL_FEATURE_INCOMPAT) + { + grub_error (GRUB_ERR_BAD_FS, "unsupported features: 0x%x", + feature & ~EROFS_ALL_FEATURE_INCOMPAT); + return NULL; + } + + data = grub_malloc (sizeof (*data)); + if (data == NULL) + return NULL; + + data->disk = disk; + data->sb = sb; + + if (read_root) + { + data->inode.data = data; + data->inode.ino = grub_le_to_cpu16 (sb.root_nid); + err = erofs_read_inode (data, &data->inode); + if (err != GRUB_ERR_NONE) + { + grub_free (data); + return NULL; + } + } + + return data; +} + +/* Context for grub_erofs_dir. */ +struct grub_erofs_dir_ctx +{ + grub_fs_dir_hook_t hook; + void *hook_data; + struct grub_erofs_data *data; +}; + +/* Helper for grub_erofs_dir. */ +static int +erofs_dir_iter (const char *filename, enum grub_fshelp_filetype filetype, + grub_fshelp_node_t node, void *data) +{ + struct grub_erofs_dir_ctx *ctx = data; + struct grub_dirhook_info info = {0}; + grub_err_t err; + + if (node->inode_loaded == false) + { + err = erofs_read_inode (ctx->data, node); + if (err != GRUB_ERR_NONE) + return 0; + } + + if (node->inode_loaded == true) + { + info.mtimeset = 1; + info.mtime = erofs_inode_mtime (node); + } + + info.dir = ((filetype & GRUB_FSHELP_TYPE_MASK) == GRUB_FSHELP_DIR); + grub_free (node); + return ctx->hook (filename, &info, ctx->hook_data); +} + +static grub_err_t +grub_erofs_dir (grub_device_t device, const char *path, grub_fs_dir_hook_t hook, + void *hook_data) +{ + grub_fshelp_node_t fdiro = NULL; + grub_err_t err; + struct grub_erofs_dir_ctx ctx = { + .hook = hook, + .hook_data = hook_data + }; + + ctx.data = erofs_mount (device->disk, true); + if (ctx.data == NULL) + goto fail; + + err = grub_fshelp_find_file (path, &ctx.data->inode, &fdiro, erofs_iterate_dir, + erofs_read_symlink, GRUB_FSHELP_DIR); + if (err != GRUB_ERR_NONE) + goto fail; + + erofs_iterate_dir (fdiro, erofs_dir_iter, &ctx); + + fail: + if (fdiro != &ctx.data->inode) + grub_free (fdiro); + grub_free (ctx.data); + + return grub_errno; +} + +static grub_err_t +grub_erofs_open (grub_file_t file, const char *name) +{ + struct grub_erofs_data *data; + struct grub_fshelp_node *fdiro = NULL; + grub_err_t err; + + data = erofs_mount (file->device->disk, true); + if (data == NULL) + { + err = grub_errno; + goto fail; + } + + err = grub_fshelp_find_file (name, &data->inode, &fdiro, erofs_iterate_dir, + erofs_read_symlink, GRUB_FSHELP_REG); + if (err != GRUB_ERR_NONE) + goto fail; + + if (fdiro->inode_loaded == false) + { + err = erofs_read_inode (data, fdiro); + if (err != GRUB_ERR_NONE) + goto fail; + } + + grub_memcpy (&data->inode, fdiro, sizeof (*fdiro)); + grub_free (fdiro); + + file->data = data; + file->size = erofs_inode_file_size (&data->inode); + + return GRUB_ERR_NONE; + + fail: + if (fdiro != &data->inode) + grub_free (fdiro); + grub_free (data); + + return err; +} + +static grub_ssize_t +grub_erofs_read (grub_file_t file, char *buf, grub_size_t len) +{ + struct grub_erofs_data *data = file->data; + struct grub_fshelp_node *inode = &data->inode; + grub_off_t off = file->offset; + grub_uint64_t ret = 0, file_size; + grub_err_t err; + + if (inode->inode_loaded == false) + { + err = erofs_read_inode (data, inode); + if (err != GRUB_ERR_NONE) + return -1; + } + + file_size = erofs_inode_file_size (inode); + + if (off > file_size) + { + grub_error (GRUB_ERR_IO, "read past EOF @ inode %" PRIuGRUB_UINT64_T, inode->ino); + return -1; + } + if (off == file_size) + return 0; + + if (off + len > file_size) + len = file_size - off; + + err = erofs_read_raw_data (inode, (grub_uint8_t *) buf, len, off, &ret); + if (err != GRUB_ERR_NONE) + return -1; + + return ret; +} + +static grub_err_t +grub_erofs_close (grub_file_t file) +{ + grub_free (file->data); + + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_erofs_uuid (grub_device_t device, char **uuid) +{ + struct grub_erofs_data *data; + + data = erofs_mount (device->disk, false); + if (data == NULL) + { + *uuid = NULL; + return grub_errno; + } + + *uuid = grub_xasprintf ("%pG", &data->sb.uuid); + + grub_free (data); + + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_erofs_label (grub_device_t device, char **label) +{ + struct grub_erofs_data *data; + + data = erofs_mount (device->disk, false); + if (data == NULL) + { + *label = NULL; + return grub_errno; + } + + *label = grub_strndup ((char *) data->sb.volume_name, sizeof (data->sb.volume_name)); + grub_free (data); + + if (*label == NULL) + return grub_errno; + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_erofs_mtime (grub_device_t device, grub_int64_t *tm) +{ + struct grub_erofs_data *data; + + data = erofs_mount (device->disk, false); + if (data == NULL) + { + *tm = 0; + return grub_errno; + } + + *tm = grub_le_to_cpu64 (data->sb.build_time); + + grub_free (data); + + return GRUB_ERR_NONE; +} + +static struct grub_fs grub_erofs_fs = { + .name = "erofs", + .fs_dir = grub_erofs_dir, + .fs_open = grub_erofs_open, + .fs_read = grub_erofs_read, + .fs_close = grub_erofs_close, + .fs_uuid = grub_erofs_uuid, + .fs_label = grub_erofs_label, + .fs_mtime = grub_erofs_mtime, +#ifdef GRUB_UTIL + .reserved_first_sector = 1, + .blocklist_install = 0, +#endif + .next = 0, +}; + +GRUB_MOD_INIT (erofs) +{ + grub_fs_register (&grub_erofs_fs); +} + +GRUB_MOD_FINI (erofs) +{ + grub_fs_unregister (&grub_erofs_fs); +}