--- /dev/null
+/* 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <grub/disk.h>
+#include <grub/dl.h>
+#include <grub/err.h>
+#include <grub/file.h>
+#include <grub/fs.h>
+#include <grub/fshelp.h>
+#include <grub/misc.h>
+#include <grub/mm.h>
+#include <grub/safemath.h>
+#include <grub/types.h>
+
+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);
+}