mirror of https://github.com/wolfSSL/wolfBoot.git
disk_fs: add read-only FAT32 filesystem support for disk boot
parent
336ee7fd56
commit
f06f081a20
|
|
@ -144,6 +144,13 @@ tools/unit-tests/unit-aes256
|
|||
tools/unit-tests/unit-chacha20
|
||||
tools/unit-tests/unit-delta
|
||||
tools/unit-tests/unit-disk
|
||||
tools/unit-tests/unit-fs-probe
|
||||
tools/unit-tests/unit-fat32
|
||||
tools/unit-tests/unit-fs-malicious
|
||||
tools/unit-tests/fat32-test.img
|
||||
tools/unit-tests/fat32-payload.bin
|
||||
tools/unit-tests/fat32-nested.bin
|
||||
tools/unit-tests/fat32-short.bin
|
||||
tools/unit-tests/unit-enc-nvm
|
||||
tools/unit-tests/unit-enc-nvm-flagshome
|
||||
tools/unit-tests/unit-extflash
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ int disk_open(int drv);
|
|||
int disk_part_read(int drv, int part, uint64_t off, uint64_t sz, uint8_t *buf);
|
||||
int disk_part_write(int drv, int part, uint64_t off, uint64_t sz, const uint8_t *buf);
|
||||
int disk_find_partition_by_label(int drv, const char *label);
|
||||
int disk_part_size(int drv, int part, uint64_t *size);
|
||||
int disk_part_count(int drv);
|
||||
|
||||
#endif /* _WOLFBOOT_DISK_H */
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
/* disk_fs.h
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot 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.
|
||||
*
|
||||
* wolfBoot 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
/* Read-only filesystem layer for wolfBoot disk boot.
|
||||
*
|
||||
* Sits between the partition layer (disk_part_read(), src/disk.c) and the
|
||||
* disk loader (src/update_disk.c), so a boot slot can name a file instead
|
||||
* of requiring the signed image at raw offset 0 of a partition.
|
||||
*
|
||||
* Supported: FAT32 (with VFAT long filenames) and ext4 (extent-mapped
|
||||
* files only). A partition holding neither is presented as a single file
|
||||
* spanning the whole partition (FS_TYPE_RAW), which is exactly the
|
||||
* pre-existing behaviour.
|
||||
*
|
||||
* SECURITY: every byte this layer parses is attacker-controlled and is
|
||||
* read BEFORE the image signature is verified. All metadata is validated
|
||||
* before use, and every loop that follows on-media links is bounded. See
|
||||
* the WOLFBOOT_FS_MAX_* knobs below.
|
||||
*
|
||||
* Compile with DISK_FS=fat32|ext4|both.
|
||||
*/
|
||||
#ifndef WOLFBOOT_DISK_FS_H
|
||||
#define WOLFBOOT_DISK_FS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "gpt.h"
|
||||
|
||||
/* Per-filesystem on-disk layout and backend interfaces. Only the parsers
|
||||
* selected by DISK_FS are compiled, so each is included on its own. */
|
||||
#ifdef WOLFBOOT_FAT32
|
||||
#include "fat32.h"
|
||||
#endif
|
||||
#ifdef WOLFBOOT_EXT4
|
||||
#include "ext4.h"
|
||||
#endif
|
||||
|
||||
/* error codes */
|
||||
#define WOLFBOOT_FS_OK 0
|
||||
#define WOLFBOOT_FS_E_PARAM -1 /* bad argument / path too long */
|
||||
#define WOLFBOOT_FS_E_IO -2 /* disk read failed or came up short */
|
||||
#define WOLFBOOT_FS_E_NOFS -3 /* nothing recognizable: use raw */
|
||||
#define WOLFBOOT_FS_E_FORMAT -4 /* metadata failed validation */
|
||||
#define WOLFBOOT_FS_E_UNSUPPORTED -5 /* valid filesystem, feature refused */
|
||||
#define WOLFBOOT_FS_E_NOENT -6 /* path component not found */
|
||||
#define WOLFBOOT_FS_E_NOTFILE -7 /* found, but not a regular file */
|
||||
#define WOLFBOOT_FS_E_TOOBIG -8 /* size exceeds the caller's max */
|
||||
#define WOLFBOOT_FS_E_CORRUPT -9 /* a security bound tripped */
|
||||
#define WOLFBOOT_FS_E_RANGE -10 /* offset arithmetic out of range */
|
||||
|
||||
/* filesystem types */
|
||||
#define FS_TYPE_NONE 0
|
||||
#define FS_TYPE_RAW 1
|
||||
#define FS_TYPE_FAT32 2
|
||||
#define FS_TYPE_EXT4 3
|
||||
|
||||
/* Metadata cache size. Holds ONLY metadata (FAT entries, directory
|
||||
* sectors, superblocks, group descriptors, inodes, extent nodes); bulk file
|
||||
* data streams straight to the caller's buffer. Raising it cuts disk reads
|
||||
* on a heavily fragmented file. */
|
||||
#ifndef WOLFBOOT_FS_CACHE_SIZE
|
||||
#define WOLFBOOT_FS_CACHE_SIZE 512
|
||||
#endif
|
||||
#if (WOLFBOOT_FS_CACHE_SIZE < 512) || ((WOLFBOOT_FS_CACHE_SIZE % 512) != 0)
|
||||
#error "WOLFBOOT_FS_CACHE_SIZE must be a multiple of 512, minimum 512"
|
||||
#endif
|
||||
|
||||
/* Security bounds. Each of these exists to make an on-media structure that
|
||||
* can reference other on-media structures terminate in bounded time even
|
||||
* when it is crafted. Each has a dedicated negative unit test. */
|
||||
|
||||
/* Longest absolute path accepted by fs_open(). */
|
||||
#ifndef WOLFBOOT_FS_MAX_PATH
|
||||
#define WOLFBOOT_FS_MAX_PATH 128
|
||||
#endif
|
||||
/* Maximum number of '/'-separated components in a path. */
|
||||
#ifndef WOLFBOOT_FS_MAX_PATH_DEPTH
|
||||
#define WOLFBOOT_FS_MAX_PATH_DEPTH 8
|
||||
#endif
|
||||
/* Longest single filename accepted. Names longer than this can never match
|
||||
* a configured path component, so they are skipped rather than assembled. */
|
||||
#ifndef WOLFBOOT_FS_MAX_NAME
|
||||
#define WOLFBOOT_FS_MAX_NAME 64
|
||||
#endif
|
||||
/* Maximum directory entries scanned before declaring the directory bad. */
|
||||
#ifndef WOLFBOOT_FS_MAX_DIR_ENTRIES
|
||||
#define WOLFBOOT_FS_MAX_DIR_ENTRIES 4096
|
||||
#endif
|
||||
/* Maximum blocks/clusters walked while scanning one directory. */
|
||||
#ifndef WOLFBOOT_FS_MAX_DIR_BLOCKS
|
||||
#define WOLFBOOT_FS_MAX_DIR_BLOCKS 1024
|
||||
#endif
|
||||
/* Absolute stop for a FAT cluster chain walk, independent of file size and
|
||||
* of volume geometry. */
|
||||
#ifndef WOLFBOOT_FS_MAX_CHAIN
|
||||
#define WOLFBOOT_FS_MAX_CHAIN 1048576
|
||||
#endif
|
||||
/* Maximum ext4 extent tree depth. This is ext4's own architectural limit;
|
||||
* the walk additionally requires depth to decrease by exactly one at every
|
||||
* level, which is what makes the descent provably finite. */
|
||||
#ifndef WOLFBOOT_FS_EXT4_MAX_DEPTH
|
||||
#define WOLFBOOT_FS_EXT4_MAX_DEPTH 5
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/* A mounted partition. FS_TYPE_RAW is the identity filesystem: the whole
|
||||
* partition is one file and fs_read() is a direct disk_part_read(), which is
|
||||
* how a DISK_FS build stays byte-identical to one without it on a partition
|
||||
* holding no filesystem. */
|
||||
struct fs_volume {
|
||||
uint64_t part_sz; /* partition size in bytes; bounds all arithmetic */
|
||||
int drv;
|
||||
int part;
|
||||
int type; /* FS_TYPE_* */
|
||||
union {
|
||||
#ifdef WOLFBOOT_FAT32
|
||||
struct fat32_volume fat;
|
||||
#endif
|
||||
#ifdef WOLFBOOT_EXT4
|
||||
struct ext4_volume ext;
|
||||
#endif
|
||||
/* Keeps the union non-empty for a RAW-only build. */
|
||||
uint8_t raw;
|
||||
} u;
|
||||
};
|
||||
|
||||
/* An open file. The forward-only cursor makes a sequential load one linear
|
||||
* metadata walk rather than one walk per read. */
|
||||
struct fs_file {
|
||||
struct fs_volume *vol;
|
||||
uint64_t size; /* validated <= partition size and <= max_size */
|
||||
uint64_t cur_off; /* file offset at which the cursor position begins */
|
||||
uint32_t first_clus; /* FAT32: first cluster of the file */
|
||||
uint32_t cur_clus; /* FAT32: cluster covering cur_off */
|
||||
uint8_t iblock[60]; /* ext4: raw i_block[], the extent tree root node */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Probe a partition and populate a volume descriptor.
|
||||
*
|
||||
* Order: wolfBoot magic at offset 0 (short-circuits to RAW), FAT32, ext4,
|
||||
* else RAW. An UNRECOGNIZED partition is not an error - it becomes RAW, so
|
||||
* raw boot behaviour is preserved. A partition that IS a filesystem we
|
||||
* refuse (FAT12/16, ext2/3, an unsupported ext4 feature) returns
|
||||
* WOLFBOOT_FS_E_UNSUPPORTED. The distinction is deliberate: falling back to
|
||||
* raw on a real filesystem would parse a boot sector as an image header.
|
||||
*/
|
||||
int fs_mount(struct fs_volume *vol, int drv, int part);
|
||||
|
||||
/**
|
||||
* @brief Resolve an absolute path to a regular file. RAW ignores the path
|
||||
* and opens the whole partition.
|
||||
*
|
||||
* @param max_size Upper bound on file size. The filesystem-reported size is
|
||||
* attacker-controlled and is checked against this before it
|
||||
* drives any I/O.
|
||||
*/
|
||||
int fs_open(struct fs_volume *vol, struct fs_file *f, const char *path,
|
||||
uint64_t max_size);
|
||||
|
||||
/* Read from an open file. Matches disk_part_read()'s contract - returns
|
||||
* bytes read (may be short at EOF), or negative WOLFBOOT_FS_E_* - so callers
|
||||
* can switch between a file and a raw partition unchanged. */
|
||||
int fs_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf);
|
||||
|
||||
/* Size in bytes of an open file. */
|
||||
uint64_t fs_size(const struct fs_file *f);
|
||||
|
||||
/* Human-readable volume type, for boot logs. */
|
||||
const char *fs_type_name(const struct fs_volume *vol);
|
||||
|
||||
/* Compare a volume's label (FAT32 BS_VolLab / ext4 s_volume_name) against
|
||||
* an ASCII string, so a partition can be selected by name on MBR disks where
|
||||
* GPT names do not exist. Case-insensitive for FAT32, case-sensitive for
|
||||
* ext4, matching each filesystem's convention.
|
||||
* Returns 1 match, 0 no match, negative WOLFBOOT_FS_E_* on error. */
|
||||
int fs_label_eq(struct fs_volume *vol, const char *label);
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Internal API. Shared between src/disk_fs.c and the filesystem backends
|
||||
* (src/fat32.c, src/ext4.c). Not intended for use outside this layer.
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
/* Byte-wise little-endian accessors. Used in preference to casting packed
|
||||
* structs onto a sector buffer, which is only correct on little-endian
|
||||
* targets and trips -Wextra on packed-member address-taking. */
|
||||
int fs_is_pow2(uint32_t v);
|
||||
|
||||
/* Deliberately out-of-line, unlike the inline readers in gpt.h. The parsers
|
||||
* call these about fifty times between them, and inlining every site costs
|
||||
* more code than the calls do. gpt.c and disk.c have only a handful of sites
|
||||
* each, so the inline form is the right trade there. */
|
||||
uint16_t fs_le16(const uint8_t *p);
|
||||
uint32_t fs_le32(const uint8_t *p);
|
||||
|
||||
/* Read exactly len bytes at a partition-relative offset, or fail.
|
||||
* disk_part_read() silently CLAMPS to the partition end and returns the
|
||||
* clamped count; treating that as success would turn a truncation into a
|
||||
* silent wrong read, so every read in this layer goes through here. */
|
||||
int fs_disk_read_exact(struct fs_volume *v, uint64_t off, uint32_t len,
|
||||
uint8_t *buf);
|
||||
|
||||
/* Read metadata through the shared cache, handling straddled windows so
|
||||
* callers never deal with alignment. Bulk file data must NOT come through
|
||||
* here: it would evict the metadata window and gain nothing. */
|
||||
int fs_meta_read(struct fs_volume *v, uint64_t off, uint32_t len,
|
||||
uint8_t *buf);
|
||||
|
||||
/* Drop the metadata cache. Called on mount, and whenever the underlying
|
||||
* media may have changed. */
|
||||
void fs_cache_invalidate(void);
|
||||
|
||||
/* Split the next component off an absolute path: advances *p, writes the
|
||||
* component NUL-terminated to out (WOLFBOOT_FS_MAX_NAME + 1 bytes), bumps
|
||||
* *depth. Rejects "." and ".." outright - neither is needed to name a boot
|
||||
* image, and ".." is the only way a crafted tree could build a traversal
|
||||
* cycle. Returns 1 for a component, 0 at end of path, negative on error. */
|
||||
int fs_path_next(const char **p, char *out, uint32_t *out_len, int *depth);
|
||||
|
||||
#endif /* WOLFBOOT_DISK_FS_H */
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/* fat32.h
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot 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.
|
||||
*
|
||||
* wolfBoot 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
/* On-disk FAT32 layout constants and the FAT32 backend interface.
|
||||
*/
|
||||
|
||||
#ifndef WOLFBOOT_FAT32_H
|
||||
#define WOLFBOOT_FAT32_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* The partition layer works in 512-byte sectors throughout (GPT_SECTOR_SIZE
|
||||
* in include/gpt.h, and the lba*512 arithmetic in src/disk.c), so a volume
|
||||
* with any other logical sector size is refused rather than misread. */
|
||||
#define FAT_SECTOR_SIZE 512U
|
||||
|
||||
/* BIOS Parameter Block field offsets within the boot sector */
|
||||
#define FAT_BPB_SIZE 64U
|
||||
#define FAT_BPB_MEDIA 0x15U
|
||||
#define FAT_BPB_BYTS_PER_SEC 0x0BU
|
||||
#define FAT_BPB_SEC_PER_CLUS 0x0DU
|
||||
#define FAT_BPB_RSVD_SEC_CNT 0x0EU
|
||||
#define FAT_BPB_NUM_FATS 0x10U
|
||||
#define FAT_BPB_ROOT_ENT_CNT 0x11U
|
||||
#define FAT_BPB_TOT_SEC16 0x13U
|
||||
#define FAT_BPB_FAT_SZ16 0x16U
|
||||
#define FAT_BPB_TOT_SEC32 0x20U
|
||||
#define FAT_BPB_FAT_SZ32 0x24U
|
||||
#define FAT_BPB_EXT_FLAGS 0x28U
|
||||
#define FAT_BPB_FS_VER 0x2AU
|
||||
#define FAT_BPB_ROOT_CLUS 0x2CU
|
||||
#define FAT_BS_VOL_LAB 0x47U
|
||||
#define FAT_BS_VOL_LAB_LEN 11U
|
||||
#define FAT_BOOT_SIG_OFF 0x1FEU
|
||||
#define FAT_BOOT_SIG_VALUE 0xAA55U
|
||||
|
||||
/* BPB_ExtFlags: bit 7 clear means all FATs are mirrored, and the low
|
||||
* nibble selects the active FAT when it is set. */
|
||||
#define FAT_EXT_NO_MIRROR 0x0080U
|
||||
#define FAT_EXT_ACTIVE_MASK 0x000FU
|
||||
|
||||
/* FAT entry values, after masking off the reserved high nibble */
|
||||
#define FAT_CLUS_MASK 0x0FFFFFFFU
|
||||
#define FAT_CLUS_MAX 0x0FFFFFF5U /* highest usable cluster number */
|
||||
#define FAT_CLUS_BAD 0x0FFFFFF7U
|
||||
#define FAT_CLUS_EOC_MIN 0x0FFFFFF8U
|
||||
|
||||
/* A volume with fewer data clusters than this is FAT16 or FAT12 by
|
||||
* definition, whatever its BPB claims. */
|
||||
#define FAT_MIN_CLUSTERS 65525U
|
||||
#define FAT_MAX_CLUS_SIZE 65536U
|
||||
|
||||
/* Directory entry layout */
|
||||
#define FAT_DIR_ENTRY_SIZE 32U
|
||||
#define FAT_DIR_ATTR 11U
|
||||
#define FAT_DIR_FST_CLUS_HI 20U
|
||||
#define FAT_DIR_FST_CLUS_LO 26U
|
||||
#define FAT_DIR_FILE_SIZE 28U
|
||||
#define FAT_DIR_FREE 0xE5U
|
||||
#define FAT_DIR_END 0x00U
|
||||
#define FAT_DIR_KANJI_E5 0x05U
|
||||
|
||||
#define FAT_ATTR_VOLUME_ID 0x08U
|
||||
#define FAT_ATTR_DIRECTORY 0x10U
|
||||
#define FAT_ATTR_LFN 0x0FU
|
||||
#define FAT_ATTR_LFN_MASK 0x3FU
|
||||
|
||||
/* Long filename entries */
|
||||
#define FAT_LFN_LAST 0x40U
|
||||
#define FAT_LFN_ORD_MASK 0x3FU
|
||||
#define FAT_LFN_MAX_ORD 20U /* 20 * 13 == 260, the FAT maximum */
|
||||
#define FAT_LFN_CHARS 13U
|
||||
#define FAT_LFN_CKSUM_OFF 13U
|
||||
|
||||
/* An 8.3 short name occupies 11 bytes ON DISK: 8 name + 3 extension, space
|
||||
* padded, with no stored '.'. FAT_SFN_MAX is the different, larger number:
|
||||
* the length of the same name once rendered as "NAME.EXT" for comparison. */
|
||||
#define FAT_SFN_NAME_LEN 8U
|
||||
#define FAT_SFN_EXT_LEN 3U
|
||||
#define FAT_SFN_LEN (FAT_SFN_NAME_LEN + FAT_SFN_EXT_LEN) /* 11 */
|
||||
#define FAT_SFN_MAX (FAT_SFN_LEN + 1U) /* 12 */
|
||||
|
||||
|
||||
/* FAT32 geometry, validated at mount. Offsets are partition-relative so
|
||||
* every read inherits disk_part_read()'s bounds checking. */
|
||||
struct fat32_volume {
|
||||
uint64_t fat_off; /* byte offset of FAT #0 within the partition */
|
||||
uint64_t data_off; /* byte offset of cluster 2 */
|
||||
uint64_t fat_sz; /* bytes in one FAT */
|
||||
uint32_t clus_sz; /* bytes per cluster */
|
||||
uint32_t clus_count; /* data clusters; highest valid is clus_count + 1 */
|
||||
uint32_t root_clus; /* first cluster of the root directory */
|
||||
};
|
||||
|
||||
struct fs_volume;
|
||||
struct fs_file;
|
||||
|
||||
/* Backend entry points. fat32_mount() returns WOLFBOOT_FS_E_NOFS when the
|
||||
* partition simply is not FAT (so probing continues), and
|
||||
* WOLFBOOT_FS_E_UNSUPPORTED when it IS FAT but uses something this
|
||||
* implementation refuses to read, such as FAT12 or FAT16. */
|
||||
int fat32_mount(struct fs_volume *vol);
|
||||
int fat32_open(struct fs_volume *vol, struct fs_file *f, const char *path,
|
||||
uint64_t max_size);
|
||||
int fat32_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf);
|
||||
int fat32_label_eq(struct fs_volume *vol, const char *label);
|
||||
|
||||
#endif /* WOLFBOOT_FAT32_H */
|
||||
45
src/disk.c
45
src/disk.c
|
|
@ -436,4 +436,49 @@ int disk_find_partition_by_label(int drv, const char *label)
|
|||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the size in bytes of a disk partition.
|
||||
*
|
||||
* The partition table stores the first and last valid byte offsets, so the
|
||||
* size is the inclusive difference plus one. Used by the read-only
|
||||
* filesystem layer to bound every offset it computes from on-media
|
||||
* metadata.
|
||||
*
|
||||
* @param[in] drv The drive number (0 to `MAX_DISKS - 1`).
|
||||
* @param[in] part The partition number (0 to `MAX_PARTITIONS - 1`).
|
||||
* @param[out] size The partition size in bytes.
|
||||
*
|
||||
* @return 0 on success, or -1 if the partition is not accessible.
|
||||
*/
|
||||
int disk_part_size(int drv, int part, uint64_t *size)
|
||||
{
|
||||
struct disk_partition *p = open_part(drv, part);
|
||||
if ((p == NULL) || (size == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if (p->end < p->start) {
|
||||
return -1;
|
||||
}
|
||||
*size = (p->end - p->start) + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the number of partitions found on a drive.
|
||||
*
|
||||
* @param[in] drv The drive number (0 to `MAX_DISKS - 1`).
|
||||
*
|
||||
* @return The number of partitions, or -1 if the drive is not open.
|
||||
*/
|
||||
int disk_part_count(int drv)
|
||||
{
|
||||
if ((drv < 0) || (drv >= MAX_DISKS)) {
|
||||
return -1;
|
||||
}
|
||||
if (Drives[drv].is_open == 0) {
|
||||
return -1;
|
||||
}
|
||||
return Drives[drv].n_parts;
|
||||
}
|
||||
|
||||
#endif /* _WOLFBOOT_DISK_C_ */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,465 @@
|
|||
/* disk_fs.c
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot 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.
|
||||
*
|
||||
* wolfBoot 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
/* Read-only filesystem layer for wolfBoot disk boot.
|
||||
*
|
||||
* Holds what is common to every backend: probe order, type dispatch, the
|
||||
* RAW (no filesystem) backend, the shared metadata cache, the little-endian
|
||||
* accessors and the path splitter. The parsers live in src/fat32.c and
|
||||
* src/ext4.c.
|
||||
*
|
||||
* Dispatch is a switch on vol->type, not a function-pointer table: taking
|
||||
* the address of a backend entry point defeats --gc-sections, so an
|
||||
* ext4-only build would still link the FAT32 code. With a switch the
|
||||
* unselected arm is preprocessed away and the linker drops it.
|
||||
*/
|
||||
|
||||
#ifndef _WOLFBOOT_DISK_FS_C_
|
||||
#define _WOLFBOOT_DISK_FS_C_
|
||||
|
||||
#ifdef WOLFBOOT_DISK_FS
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "disk_fs.h"
|
||||
#include "disk.h"
|
||||
#include "printf.h"
|
||||
#include "wolfboot/wolfboot.h"
|
||||
|
||||
#ifdef DEBUG_FS
|
||||
#define FS_DBG(_f_, ...) wolfBoot_printf(_f_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define FS_DBG(_f_, ...) do{}while(0)
|
||||
#endif
|
||||
|
||||
/* wolfBoot image header magic as it appears on media. Compared byte-wise,
|
||||
* not as a word: WOLFBOOT_MAGIC has the opposite byte order on big-endian
|
||||
* builds, so a word compare would only be right on one of them. */
|
||||
static const uint8_t fs_wolfboot_magic[4] = { 'W', 'O', 'L', 'F' };
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Metadata cache: one statically allocated window (several disk targets
|
||||
* build with WOLFBOOT_SMALL_STACK=1, so the stack is not an option). Keyed
|
||||
* by drive and partition as well as offset - slot A and slot B can be
|
||||
* independent partitions, and an offset-only key would hand slot B's reads
|
||||
* slot A's cached bytes.
|
||||
* --------------------------------------------------------------------- */
|
||||
static uint8_t fs_cache[WOLFBOOT_FS_CACHE_SIZE] XALIGNED(4);
|
||||
static uint64_t fs_cache_off;
|
||||
static uint32_t fs_cache_len;
|
||||
static int fs_cache_drv;
|
||||
static int fs_cache_part;
|
||||
static int fs_cache_valid;
|
||||
|
||||
void fs_cache_invalidate(void)
|
||||
{
|
||||
fs_cache_valid = 0;
|
||||
fs_cache_off = 0;
|
||||
fs_cache_len = 0;
|
||||
fs_cache_drv = -1;
|
||||
fs_cache_part = -1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Little-endian accessors
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
int fs_is_pow2(uint32_t v)
|
||||
{
|
||||
return ((v != 0U) && ((v & (v - 1U)) == 0U)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Bounded disk access
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
uint16_t fs_le16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
uint32_t fs_le32(const uint8_t *p)
|
||||
{
|
||||
return ((uint32_t)p[0]) |
|
||||
((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) |
|
||||
((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
int fs_disk_read_exact(struct fs_volume *v, uint64_t off, uint32_t len,
|
||||
uint8_t *buf)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if ((v == NULL) || (buf == NULL)) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
if (len == 0U) {
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
/* Bound against the partition before issuing the read, so an offset
|
||||
* derived from crafted metadata is rejected here rather than being
|
||||
* quietly truncated by disk_part_read(). */
|
||||
if ((off > v->part_sz) || ((uint64_t)len > (v->part_sz - off))) {
|
||||
FS_DBG("fs: read out of range off %x_%x len %u part %x_%x\r\n",
|
||||
(uint32_t)(off >> 32), (uint32_t)off, len,
|
||||
(uint32_t)(v->part_sz >> 32), (uint32_t)v->part_sz);
|
||||
return WOLFBOOT_FS_E_RANGE;
|
||||
}
|
||||
ret = disk_part_read(v->drv, v->part, off, (uint64_t)len, buf);
|
||||
if (ret < 0) {
|
||||
return WOLFBOOT_FS_E_IO;
|
||||
}
|
||||
/* disk_part_read() clamps to the partition end and reports the clamped
|
||||
* count. Anything short of what was asked for is an I/O error here. */
|
||||
if ((uint32_t)ret != len) {
|
||||
FS_DBG("fs: short read %d of %u\r\n", ret, len);
|
||||
return WOLFBOOT_FS_E_IO;
|
||||
}
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
int fs_meta_read(struct fs_volume *v, uint64_t off, uint32_t len,
|
||||
uint8_t *buf)
|
||||
{
|
||||
uint64_t pos, win, avail;
|
||||
uint32_t in_win, chunk, rdlen, done;
|
||||
int ret;
|
||||
|
||||
if ((v == NULL) || (buf == NULL)) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
if (len == 0U) {
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
if ((off > v->part_sz) || ((uint64_t)len > (v->part_sz - off))) {
|
||||
return WOLFBOOT_FS_E_RANGE;
|
||||
}
|
||||
|
||||
done = 0U;
|
||||
while (done < len) {
|
||||
pos = off + (uint64_t)done;
|
||||
win = pos - (pos % (uint64_t)WOLFBOOT_FS_CACHE_SIZE);
|
||||
|
||||
if ((fs_cache_valid == 0) || (fs_cache_off != win) ||
|
||||
(fs_cache_drv != v->drv) || (fs_cache_part != v->part)) {
|
||||
avail = v->part_sz - win;
|
||||
rdlen = WOLFBOOT_FS_CACHE_SIZE;
|
||||
if ((uint64_t)rdlen > avail) {
|
||||
rdlen = (uint32_t)avail;
|
||||
}
|
||||
fs_cache_valid = 0;
|
||||
ret = fs_disk_read_exact(v, win, rdlen, fs_cache);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
fs_cache_off = win;
|
||||
fs_cache_len = rdlen;
|
||||
fs_cache_drv = v->drv;
|
||||
fs_cache_part = v->part;
|
||||
fs_cache_valid = 1;
|
||||
}
|
||||
|
||||
in_win = (uint32_t)(pos - win);
|
||||
if (in_win >= fs_cache_len) {
|
||||
return WOLFBOOT_FS_E_RANGE;
|
||||
}
|
||||
chunk = fs_cache_len - in_win;
|
||||
if (chunk > (len - done)) {
|
||||
chunk = len - done;
|
||||
}
|
||||
memcpy(buf + done, fs_cache + in_win, chunk);
|
||||
done += chunk;
|
||||
}
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Path handling
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
int fs_path_next(const char **p, char *out, uint32_t *out_len, int *depth)
|
||||
{
|
||||
const char *s;
|
||||
uint32_t n;
|
||||
|
||||
if ((p == NULL) || (*p == NULL) || (out == NULL) || (out_len == NULL) ||
|
||||
(depth == NULL)) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
s = *p;
|
||||
|
||||
/* Skip separators. Repeated slashes are harmless and are collapsed. */
|
||||
while (*s == '/') {
|
||||
s++;
|
||||
}
|
||||
if (*s == '\0') {
|
||||
*p = s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (*depth >= WOLFBOOT_FS_MAX_PATH_DEPTH) {
|
||||
FS_DBG("fs: path too deep\r\n");
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
|
||||
n = 0U;
|
||||
while ((*s != '/') && (*s != '\0')) {
|
||||
/* A backslash is an ordinary filename character on both FAT and
|
||||
* ext4. Treating it as a separator here would let a crafted name
|
||||
* be read as two components. */
|
||||
if (n >= WOLFBOOT_FS_MAX_NAME) {
|
||||
FS_DBG("fs: path component too long\r\n");
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
out[n] = *s;
|
||||
n++;
|
||||
s++;
|
||||
}
|
||||
out[n] = '\0';
|
||||
|
||||
/* "." and ".." are never needed to name a boot image, and ".." is the
|
||||
* only way a crafted directory tree could build a traversal cycle. */
|
||||
if ((n == 1U) && (out[0] == '.')) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
if ((n == 2U) && (out[0] == '.') && (out[1] == '.')) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
|
||||
*out_len = n;
|
||||
*p = s;
|
||||
(*depth)++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Public API
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
int fs_mount(struct fs_volume *vol, int drv, int part)
|
||||
{
|
||||
uint8_t magic[4];
|
||||
uint64_t sz = 0;
|
||||
int ret;
|
||||
|
||||
if (vol == NULL) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
memset(vol, 0, sizeof(*vol));
|
||||
vol->drv = drv;
|
||||
vol->part = part;
|
||||
vol->type = FS_TYPE_NONE;
|
||||
|
||||
if (disk_part_size(drv, part, &sz) != 0) {
|
||||
return WOLFBOOT_FS_E_IO;
|
||||
}
|
||||
if (sz == 0U) {
|
||||
return WOLFBOOT_FS_E_IO;
|
||||
}
|
||||
vol->part_sz = sz;
|
||||
fs_cache_invalidate();
|
||||
|
||||
/* A partition that starts with a wolfBoot image header is a raw slot.
|
||||
* Short-circuiting here means an existing raw layout is never handed
|
||||
* to a filesystem probe at all. */
|
||||
if (sz >= 4U) {
|
||||
ret = fs_meta_read(vol, 0, 4, magic);
|
||||
if ((ret == WOLFBOOT_FS_OK) &&
|
||||
(memcmp(magic, fs_wolfboot_magic, 4) == 0)) {
|
||||
vol->type = FS_TYPE_RAW;
|
||||
FS_DBG("fs: drv %d part %d: wolfBoot image at offset 0\r\n",
|
||||
drv, part);
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WOLFBOOT_FAT32
|
||||
ret = fat32_mount(vol);
|
||||
if (ret == WOLFBOOT_FS_OK) {
|
||||
vol->type = FS_TYPE_FAT32;
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
/* Anything other than "this is not FAT" stops the probe. A volume we
|
||||
* recognize but refuse must not fall through to raw, where its boot
|
||||
* sector would be parsed as an image header. */
|
||||
if (ret != WOLFBOOT_FS_E_NOFS) {
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WOLFBOOT_EXT4
|
||||
ret = ext4_mount(vol);
|
||||
if (ret == WOLFBOOT_FS_OK) {
|
||||
vol->type = FS_TYPE_EXT4;
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
if (ret != WOLFBOOT_FS_E_NOFS) {
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
vol->type = FS_TYPE_RAW;
|
||||
FS_DBG("fs: drv %d part %d: no filesystem, using raw\r\n", drv, part);
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
int fs_open(struct fs_volume *vol, struct fs_file *f, const char *path,
|
||||
uint64_t max_size)
|
||||
{
|
||||
#if defined(WOLFBOOT_FAT32) || defined(WOLFBOOT_EXT4)
|
||||
size_t plen;
|
||||
#endif
|
||||
|
||||
if ((vol == NULL) || (f == NULL)) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->vol = vol;
|
||||
|
||||
if (vol->type == FS_TYPE_RAW) {
|
||||
/* The whole partition is the file. No max_size check here: the raw
|
||||
* path has always read a header first and bounded the payload from
|
||||
* it, and changing that would alter existing behaviour. */
|
||||
(void)path;
|
||||
(void)max_size;
|
||||
f->size = vol->part_sz;
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
#if defined(WOLFBOOT_FAT32) || defined(WOLFBOOT_EXT4)
|
||||
if (path == NULL) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
if (path[0] != '/') {
|
||||
FS_DBG("fs: path must be absolute\r\n");
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
plen = strlen(path);
|
||||
if (plen > (size_t)WOLFBOOT_FS_MAX_PATH) {
|
||||
FS_DBG("fs: path too long\r\n");
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WOLFBOOT_FAT32
|
||||
if (vol->type == FS_TYPE_FAT32) {
|
||||
return fat32_open(vol, f, path, max_size);
|
||||
}
|
||||
#endif
|
||||
#ifdef WOLFBOOT_EXT4
|
||||
if (vol->type == FS_TYPE_EXT4) {
|
||||
return ext4_open(vol, f, path, max_size);
|
||||
}
|
||||
#endif
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
|
||||
int fs_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if ((f == NULL) || (f->vol == NULL) || (buf == NULL)) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
if (len == 0U) {
|
||||
return 0;
|
||||
}
|
||||
if (off >= f->size) {
|
||||
return 0;
|
||||
}
|
||||
if (len > (f->size - off)) {
|
||||
len = f->size - off;
|
||||
}
|
||||
if (len > (uint64_t)DISK_IO_MAX_SIZE) {
|
||||
len = (uint64_t)DISK_IO_MAX_SIZE;
|
||||
}
|
||||
|
||||
switch (f->vol->type) {
|
||||
case FS_TYPE_RAW:
|
||||
ret = disk_part_read(f->vol->drv, f->vol->part, off, len, buf);
|
||||
break;
|
||||
#ifdef WOLFBOOT_FAT32
|
||||
case FS_TYPE_FAT32:
|
||||
ret = fat32_read(f, off, len, buf);
|
||||
break;
|
||||
#endif
|
||||
#ifdef WOLFBOOT_EXT4
|
||||
case FS_TYPE_EXT4:
|
||||
ret = ext4_read(f, off, len, buf);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
ret = WOLFBOOT_FS_E_PARAM;
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t fs_size(const struct fs_file *f)
|
||||
{
|
||||
if (f == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return f->size;
|
||||
}
|
||||
|
||||
const char *fs_type_name(const struct fs_volume *vol)
|
||||
{
|
||||
if (vol == NULL) {
|
||||
return "none";
|
||||
}
|
||||
switch (vol->type) {
|
||||
case FS_TYPE_RAW:
|
||||
return "raw";
|
||||
case FS_TYPE_FAT32:
|
||||
return "fat32";
|
||||
case FS_TYPE_EXT4:
|
||||
return "ext4";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
int fs_label_eq(struct fs_volume *vol, const char *label)
|
||||
{
|
||||
if ((vol == NULL) || (label == NULL)) {
|
||||
return WOLFBOOT_FS_E_PARAM;
|
||||
}
|
||||
switch (vol->type) {
|
||||
#ifdef WOLFBOOT_FAT32
|
||||
case FS_TYPE_FAT32:
|
||||
return fat32_label_eq(vol, label);
|
||||
#endif
|
||||
#ifdef WOLFBOOT_EXT4
|
||||
case FS_TYPE_EXT4:
|
||||
return ext4_label_eq(vol, label);
|
||||
#endif
|
||||
default:
|
||||
/* A raw partition has no filesystem label. */
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* WOLFBOOT_DISK_FS */
|
||||
|
||||
#endif /* _WOLFBOOT_DISK_FS_C_ */
|
||||
|
|
@ -0,0 +1,810 @@
|
|||
/* fat32.c
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot 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.
|
||||
*
|
||||
* wolfBoot 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
/* Read-only FAT32 backend for the wolfBoot disk filesystem layer.
|
||||
*
|
||||
* Supports FAT32 only, with VFAT long filename reads. FAT12 and FAT16 are
|
||||
* refused rather than misread, as are volumes with a logical sector size
|
||||
* other than 512 (the partition layer assumes 512 throughout).
|
||||
*
|
||||
* SECURITY: the boot sector, the FAT and the directory entries are all
|
||||
* attacker-controlled and are parsed before the image signature is
|
||||
* checked. Every geometry field is validated at mount time so that later
|
||||
* cluster-to-byte arithmetic cannot leave the partition, and every walk
|
||||
* over an on-media link is bounded so a crafted cycle terminates.
|
||||
*/
|
||||
|
||||
#ifndef _WOLFBOOT_FAT32_C_
|
||||
#define _WOLFBOOT_FAT32_C_
|
||||
|
||||
#if defined(WOLFBOOT_DISK_FS) && defined(WOLFBOOT_FAT32)
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "disk_fs.h"
|
||||
#include "fat32.h"
|
||||
#include "disk.h"
|
||||
#include "printf.h"
|
||||
|
||||
#ifdef DEBUG_FS
|
||||
#define FAT_DBG(_f_, ...) wolfBoot_printf(_f_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define FAT_DBG(_f_, ...) do{}while(0)
|
||||
#endif
|
||||
|
||||
/* Accumulator for one long filename set. LFN entries precede their short
|
||||
* entry in reverse ordinal order, so the set is assembled as it is walked
|
||||
* and only committed once the short entry passes the checksum test. */
|
||||
struct fat_lfn_state {
|
||||
char name[WOLFBOOT_FS_MAX_NAME + 1];
|
||||
uint8_t expect_ord;
|
||||
uint8_t cksum;
|
||||
uint8_t have;
|
||||
};
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Name handling
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
static char fat32_upper(char c)
|
||||
{
|
||||
if ((c >= 'a') && (c <= 'z')) {
|
||||
return (char)(c - ('a' - 'A'));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/* FAT filenames are case-insensitive. */
|
||||
static int fat32_name_eq(const char *a, const char *b)
|
||||
{
|
||||
uint32_t i = 0;
|
||||
|
||||
while ((a[i] != '\0') && (b[i] != '\0')) {
|
||||
if (fat32_upper(a[i]) != fat32_upper(b[i])) {
|
||||
return 0;
|
||||
}
|
||||
i++;
|
||||
if (i > WOLFBOOT_FS_MAX_NAME) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return (a[i] == b[i]) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Rotate-and-add checksum of an 8.3 name, per the FAT spec. This is the
|
||||
* ONLY thing binding an LFN set to the short entry after it: unverified, a
|
||||
* crafted directory could pair an attacker-chosen long name with an
|
||||
* unrelated file's cluster and size, opening a different file than the one
|
||||
* configured - before any signature check runs. */
|
||||
static uint8_t fat32_sfn_checksum(const uint8_t *sfn)
|
||||
{
|
||||
uint8_t sum = 0;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0; i < FAT_SFN_LEN; i++) {
|
||||
sum = (uint8_t)((uint8_t)((sum >> 1) | (uint8_t)(sum << 7)) + sfn[i]);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/* Render the 8.3 name of a directory entry as "NAME.EXT". */
|
||||
static void fat32_sfn_to_name(const uint8_t *e, char *out)
|
||||
{
|
||||
uint32_t i;
|
||||
uint32_t n = 0;
|
||||
uint8_t c;
|
||||
|
||||
for (i = 0; i < FAT_SFN_NAME_LEN; i++) {
|
||||
c = e[i];
|
||||
if (c == ' ') {
|
||||
break;
|
||||
}
|
||||
/* 0x05 stands in for a leading 0xE5, which marks a free entry. */
|
||||
if ((i == 0U) && (c == FAT_DIR_KANJI_E5)) {
|
||||
c = FAT_DIR_FREE;
|
||||
}
|
||||
out[n] = (char)c;
|
||||
n++;
|
||||
}
|
||||
if (e[FAT_SFN_NAME_LEN] != ' ') {
|
||||
out[n] = '.';
|
||||
n++;
|
||||
for (i = FAT_SFN_NAME_LEN; i < FAT_SFN_LEN; i++) {
|
||||
c = e[i];
|
||||
if (c == ' ') {
|
||||
break;
|
||||
}
|
||||
out[n] = (char)c;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
/* Fold one LFN entry into the accumulator. Any inconsistency (bad or
|
||||
* out-of-sequence ordinal, changed checksum, non-ASCII, over-long name)
|
||||
* discards the set. Not fatal: the entry just becomes unmatchable by its
|
||||
* long name and the short name is still tried. */
|
||||
static void fat32_lfn_accum(struct fat_lfn_state *s, const uint8_t *e)
|
||||
{
|
||||
/* Byte offsets of the 13 UCS-2 units within a long filename entry. */
|
||||
static const uint8_t chr_off[FAT_LFN_CHARS] = {
|
||||
1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30
|
||||
};
|
||||
uint8_t ord = (uint8_t)(e[0] & FAT_LFN_ORD_MASK);
|
||||
uint32_t base;
|
||||
uint32_t i;
|
||||
uint16_t uc;
|
||||
|
||||
if ((e[0] & FAT_LFN_LAST) != 0U) {
|
||||
memset(s, 0, sizeof(*s));
|
||||
if ((ord == 0U) || (ord > FAT_LFN_MAX_ORD)) {
|
||||
return;
|
||||
}
|
||||
/* Drop a set whose FIRST character already lies past the buffer:
|
||||
* that name can never match a configured path component.
|
||||
*
|
||||
* The bound is on the first index, not ord * FAT_LFN_CHARS. The last
|
||||
* entry holds 1 to 13 characters, so multiplying overestimates the
|
||||
* length by up to 12 and would discard every 5-entry set, i.e. every
|
||||
* name of 53 to 64 characters, even though those fit. The
|
||||
* per-character check below rejects the ones that genuinely do not. */
|
||||
if ((((uint32_t)ord - 1U) * FAT_LFN_CHARS) >=
|
||||
(uint32_t)WOLFBOOT_FS_MAX_NAME) {
|
||||
return;
|
||||
}
|
||||
s->cksum = e[FAT_LFN_CKSUM_OFF];
|
||||
s->expect_ord = ord;
|
||||
s->have = 1;
|
||||
}
|
||||
else {
|
||||
if ((s->have == 0U) || (ord != s->expect_ord) ||
|
||||
(e[FAT_LFN_CKSUM_OFF] != s->cksum)) {
|
||||
memset(s, 0, sizeof(*s));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
base = ((uint32_t)ord - 1U) * FAT_LFN_CHARS;
|
||||
for (i = 0; i < FAT_LFN_CHARS; i++) {
|
||||
uc = fs_le16(e + chr_off[i]);
|
||||
if ((uc == 0x0000U) || (uc == 0xFFFFU)) {
|
||||
break;
|
||||
}
|
||||
/* Configured paths are ASCII, so a name that is not cannot match.
|
||||
* Dropping the set avoids having to define a lossy mapping. */
|
||||
if (uc > 0x7FU) {
|
||||
memset(s, 0, sizeof(*s));
|
||||
return;
|
||||
}
|
||||
if ((base + i) >= (uint32_t)WOLFBOOT_FS_MAX_NAME) {
|
||||
memset(s, 0, sizeof(*s));
|
||||
return;
|
||||
}
|
||||
s->name[base + i] = (char)uc;
|
||||
}
|
||||
s->expect_ord--;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Mount
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
int fat32_mount(struct fs_volume *vol)
|
||||
{
|
||||
uint8_t bpb[FAT_BPB_SIZE];
|
||||
uint8_t sig[2];
|
||||
struct fat32_volume *fv;
|
||||
uint32_t byts_per_sec, sec_per_clus, rsvd, num_fats;
|
||||
uint32_t tot_sec32, fat_sz32, root_clus, clus_count, clus_sz;
|
||||
uint16_t ext_flags;
|
||||
uint64_t fat_off, fat_sz, data_off, meta_secs, data_secs;
|
||||
uint8_t media;
|
||||
int ret;
|
||||
|
||||
fv = &vol->u.fat;
|
||||
|
||||
if (vol->part_sz < (uint64_t)FAT_SECTOR_SIZE) {
|
||||
return WOLFBOOT_FS_E_NOFS;
|
||||
}
|
||||
ret = fs_meta_read(vol, 0, FAT_BPB_SIZE, bpb);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
ret = fs_meta_read(vol, FAT_BOOT_SIG_OFF, 2, sig);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
byts_per_sec = fs_le16(bpb + FAT_BPB_BYTS_PER_SEC);
|
||||
sec_per_clus = bpb[FAT_BPB_SEC_PER_CLUS];
|
||||
rsvd = fs_le16(bpb + FAT_BPB_RSVD_SEC_CNT);
|
||||
num_fats = bpb[FAT_BPB_NUM_FATS];
|
||||
media = bpb[FAT_BPB_MEDIA];
|
||||
|
||||
/* Stage 1: is this a FAT volume at all? These are the fields every FAT
|
||||
* variant shares, so failing any of them means "not FAT" and the probe
|
||||
* moves on to the next filesystem rather than reporting an error. */
|
||||
if (fs_le16(sig) != FAT_BOOT_SIG_VALUE) {
|
||||
return WOLFBOOT_FS_E_NOFS;
|
||||
}
|
||||
if ((media != 0xF0U) && (media < 0xF8U)) {
|
||||
return WOLFBOOT_FS_E_NOFS;
|
||||
}
|
||||
if ((fs_is_pow2(byts_per_sec) == 0) || (byts_per_sec < 512U) ||
|
||||
(byts_per_sec > 4096U)) {
|
||||
return WOLFBOOT_FS_E_NOFS;
|
||||
}
|
||||
if ((num_fats < 1U) || (num_fats > 2U)) {
|
||||
return WOLFBOOT_FS_E_NOFS;
|
||||
}
|
||||
if (rsvd == 0U) {
|
||||
return WOLFBOOT_FS_E_NOFS;
|
||||
}
|
||||
|
||||
/* Stage 2: it is FAT. From here on a failure is an error, never a
|
||||
* fallback to raw -- reading a real boot sector as an image header
|
||||
* would be a silent wrong result. */
|
||||
|
||||
if (byts_per_sec != FAT_SECTOR_SIZE) {
|
||||
FAT_DBG("fat32: sector size %u unsupported\r\n", byts_per_sec);
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
/* A non-zero root entry count, 16-bit total sector count or 16-bit FAT
|
||||
* size all mean FAT12/FAT16. */
|
||||
if (fs_le16(bpb + FAT_BPB_ROOT_ENT_CNT) != 0U) {
|
||||
FAT_DBG("fat32: FAT12/FAT16 volume\r\n");
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
if (fs_le16(bpb + FAT_BPB_TOT_SEC16) != 0U) {
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
if (fs_le16(bpb + FAT_BPB_FAT_SZ16) != 0U) {
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
if (fs_le16(bpb + FAT_BPB_FS_VER) != 0U) {
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
|
||||
/* With mirroring disabled, only FAT #0 being the active one is safe:
|
||||
* otherwise the chain we would follow is a stale copy. */
|
||||
ext_flags = fs_le16(bpb + FAT_BPB_EXT_FLAGS);
|
||||
if (((ext_flags & FAT_EXT_NO_MIRROR) != 0U) &&
|
||||
((ext_flags & FAT_EXT_ACTIVE_MASK) != 0U)) {
|
||||
FAT_DBG("fat32: active FAT is not FAT 0\r\n");
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
|
||||
if ((fs_is_pow2(sec_per_clus) == 0) || (sec_per_clus > 128U)) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
clus_sz = FAT_SECTOR_SIZE * sec_per_clus;
|
||||
if (clus_sz > FAT_MAX_CLUS_SIZE) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
|
||||
tot_sec32 = fs_le32(bpb + FAT_BPB_TOT_SEC32);
|
||||
fat_sz32 = fs_le32(bpb + FAT_BPB_FAT_SZ32);
|
||||
root_clus = fs_le32(bpb + FAT_BPB_ROOT_CLUS);
|
||||
|
||||
if ((tot_sec32 == 0U) || (fat_sz32 == 0U)) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
/* The volume must fit inside its partition; every byte offset derived
|
||||
* below is then bounded by construction. */
|
||||
if (((uint64_t)tot_sec32 * FAT_SECTOR_SIZE) > vol->part_sz) {
|
||||
FAT_DBG("fat32: volume larger than partition\r\n");
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
|
||||
fat_off = (uint64_t)rsvd * FAT_SECTOR_SIZE;
|
||||
fat_sz = (uint64_t)fat_sz32 * FAT_SECTOR_SIZE;
|
||||
data_off = fat_off + ((uint64_t)num_fats * fat_sz);
|
||||
|
||||
meta_secs = (uint64_t)rsvd + ((uint64_t)num_fats * (uint64_t)fat_sz32);
|
||||
if (meta_secs >= (uint64_t)tot_sec32) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
data_secs = (uint64_t)tot_sec32 - meta_secs;
|
||||
clus_count = (uint32_t)(data_secs / (uint64_t)sec_per_clus);
|
||||
|
||||
/* Fewer than 65525 data clusters is the definition of FAT16/FAT12.
|
||||
* Checking it here makes the "no FAT12/FAT16" guarantee hold even if
|
||||
* the individual BPB fields were forged to look like FAT32. */
|
||||
if (clus_count < FAT_MIN_CLUSTERS) {
|
||||
FAT_DBG("fat32: %u clusters, below the FAT32 minimum\r\n",
|
||||
clus_count);
|
||||
return WOLFBOOT_FS_E_UNSUPPORTED;
|
||||
}
|
||||
if (clus_count > FAT_CLUS_MAX) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
/* The FAT must be large enough to hold an entry for every cluster.
|
||||
* Without this, a chain could name a cluster whose FAT entry lies past
|
||||
* the end of the FAT, so the "link" read would be adjacent file data. */
|
||||
if (fat_sz < (((uint64_t)clus_count + 2U) * 4U)) {
|
||||
FAT_DBG("fat32: FAT too small for %u clusters\r\n", clus_count);
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
if ((data_off + ((uint64_t)clus_count * (uint64_t)clus_sz)) >
|
||||
vol->part_sz) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
if ((root_clus < 2U) || (root_clus > (clus_count + 1U))) {
|
||||
return WOLFBOOT_FS_E_FORMAT;
|
||||
}
|
||||
|
||||
fv->fat_off = fat_off;
|
||||
fv->fat_sz = fat_sz;
|
||||
fv->data_off = data_off;
|
||||
fv->clus_sz = clus_sz;
|
||||
fv->clus_count = clus_count;
|
||||
fv->root_clus = root_clus;
|
||||
|
||||
FAT_DBG("fat32: %u clusters of %u bytes, data at %x_%x\r\n",
|
||||
clus_count, clus_sz, (uint32_t)(data_off >> 32), (uint32_t)data_off);
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Cluster chain
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
/* A cluster number is usable only if it names a real data cluster: 0 and 1
|
||||
* are reserved, and the volume's clus_count clusters are numbered from 2. */
|
||||
static int fat32_clus_ok(const struct fat32_volume *fv, uint32_t clus)
|
||||
{
|
||||
return ((clus >= 2U) && (clus <= (fv->clus_count + 1U))) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Follow one link of a cluster chain. Returns 0 with *next set, 1 at end
|
||||
* of chain, or negative WOLFBOOT_FS_E_* on error. */
|
||||
static int fat32_next_clus(struct fs_volume *vol, uint32_t clus,
|
||||
uint32_t *next)
|
||||
{
|
||||
struct fat32_volume *fv = &vol->u.fat;
|
||||
uint8_t ent[4];
|
||||
uint64_t off;
|
||||
uint32_t val;
|
||||
int ret;
|
||||
|
||||
if (fat32_clus_ok(fv, clus) == 0) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
/* clus is bounded by FAT_CLUS_MAX, so this cannot overflow. */
|
||||
off = fv->fat_off + ((uint64_t)clus * 4U);
|
||||
if ((off + 4U) > (fv->fat_off + fv->fat_sz)) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
ret = fs_meta_read(vol, off, 4, ent);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
val = fs_le32(ent) & FAT_CLUS_MASK;
|
||||
|
||||
if (val >= FAT_CLUS_EOC_MIN) {
|
||||
*next = 0;
|
||||
return 1;
|
||||
}
|
||||
if (val == FAT_CLUS_BAD) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
/* Free or reserved, or past the end of the volume: a chain must never
|
||||
* point at one of these. */
|
||||
if (fat32_clus_ok(fv, val) == 0) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
*next = val;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Position the cursor on the cluster holding a file offset. Forward-only, so
|
||||
* a sequential load walks the chain once; a backwards seek restarts at the
|
||||
* head. Bounded three ways: by the clusters the length can occupy, by the
|
||||
* volume's cluster count (longer means a cycle), and by an absolute stop. A
|
||||
* visited-set is unaffordable at 2^28 clusters. */
|
||||
static int fat32_seek(struct fs_file *f, uint64_t off)
|
||||
{
|
||||
struct fs_volume *vol = f->vol;
|
||||
struct fat32_volume *fv = &vol->u.fat;
|
||||
uint64_t target, cur_idx, max_clusters, steps;
|
||||
uint32_t next;
|
||||
int ret;
|
||||
|
||||
target = off / (uint64_t)fv->clus_sz;
|
||||
|
||||
if ((f->cur_clus < 2U) ||
|
||||
(target < (f->cur_off / (uint64_t)fv->clus_sz))) {
|
||||
f->cur_clus = f->first_clus;
|
||||
f->cur_off = 0;
|
||||
}
|
||||
if (fat32_clus_ok(fv, f->cur_clus) == 0) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
|
||||
max_clusters = (f->size + (uint64_t)fv->clus_sz - 1U) /
|
||||
(uint64_t)fv->clus_sz;
|
||||
cur_idx = f->cur_off / (uint64_t)fv->clus_sz;
|
||||
steps = 0;
|
||||
|
||||
while (cur_idx < target) {
|
||||
ret = fat32_next_clus(vol, f->cur_clus, &next);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
if (ret == 1) {
|
||||
/* The chain ended before the file's declared length. */
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
f->cur_clus = next;
|
||||
f->cur_off += (uint64_t)fv->clus_sz;
|
||||
cur_idx++;
|
||||
steps++;
|
||||
if ((steps > max_clusters) || (steps > (uint64_t)fv->clus_count) ||
|
||||
(steps > (uint64_t)WOLFBOOT_FS_MAX_CHAIN)) {
|
||||
FAT_DBG("fat32: cluster chain bound tripped\r\n");
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
}
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
/* Walk the whole chain at open time, so a malformed one errors before any
|
||||
* payload loads. A cycle never hits end-of-chain, so it overruns the cluster
|
||||
* count and trips that bound; no visited-set needed. A chain LONGER than the
|
||||
* length needs is fine (preallocating writers do this); only short is an
|
||||
* error. */
|
||||
static int fat32_validate_chain(struct fs_volume *vol, uint32_t first,
|
||||
uint64_t size)
|
||||
{
|
||||
struct fat32_volume *fv = &vol->u.fat;
|
||||
uint64_t need, i, limit;
|
||||
uint32_t clus = first;
|
||||
uint32_t next;
|
||||
int ret;
|
||||
|
||||
if (size == 0U) {
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
need = (size + (uint64_t)fv->clus_sz - 1U) / (uint64_t)fv->clus_sz;
|
||||
if (need > (uint64_t)fv->clus_count) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
|
||||
limit = (uint64_t)fv->clus_count;
|
||||
if (limit > (uint64_t)WOLFBOOT_FS_MAX_CHAIN) {
|
||||
limit = (uint64_t)WOLFBOOT_FS_MAX_CHAIN;
|
||||
}
|
||||
|
||||
for (i = 1; i <= limit; i++) {
|
||||
ret = fat32_next_clus(vol, clus, &next);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
if (ret == 1) {
|
||||
/* The chain has i clusters. Fewer than the declared length
|
||||
* needs means the directory entry and the FAT disagree. */
|
||||
if (i < need) {
|
||||
FAT_DBG("fat32: chain shorter than file size\r\n");
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
clus = next;
|
||||
}
|
||||
FAT_DBG("fat32: cluster chain never terminates\r\n");
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Directory lookup
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
static int fat32_lookup(struct fs_volume *vol, uint32_t dir_clus,
|
||||
const char *name, uint32_t *out_clus,
|
||||
uint32_t *out_size, uint8_t *out_attr)
|
||||
{
|
||||
struct fat32_volume *fv = &vol->u.fat;
|
||||
struct fat_lfn_state lfn;
|
||||
uint8_t e[FAT_DIR_ENTRY_SIZE];
|
||||
char sfn[FAT_SFN_MAX + 1];
|
||||
uint64_t base;
|
||||
uint32_t clus = dir_clus;
|
||||
uint32_t blocks = 0;
|
||||
uint32_t entries = 0;
|
||||
uint32_t ents_per_clus;
|
||||
uint32_t idx;
|
||||
uint32_t next;
|
||||
uint8_t attr;
|
||||
int matched;
|
||||
int ret;
|
||||
|
||||
ents_per_clus = fv->clus_sz / FAT_DIR_ENTRY_SIZE;
|
||||
memset(&lfn, 0, sizeof(lfn));
|
||||
|
||||
for (;;) {
|
||||
/* A directory chain has no declared length, so it is bounded by
|
||||
* the volume's cluster count (anything longer is a cycle) and by
|
||||
* the configurable block cap. */
|
||||
if ((blocks >= (uint32_t)WOLFBOOT_FS_MAX_DIR_BLOCKS) ||
|
||||
(blocks >= fv->clus_count)) {
|
||||
FAT_DBG("fat32: directory chain bound tripped\r\n");
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
if (fat32_clus_ok(fv, clus) == 0) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
base = fv->data_off + (((uint64_t)clus - 2U) * (uint64_t)fv->clus_sz);
|
||||
|
||||
for (idx = 0; idx < ents_per_clus; idx++) {
|
||||
if (entries >= (uint32_t)WOLFBOOT_FS_MAX_DIR_ENTRIES) {
|
||||
FAT_DBG("fat32: directory entry bound tripped\r\n");
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
entries++;
|
||||
|
||||
ret = fs_meta_read(vol,
|
||||
base + ((uint64_t)idx * FAT_DIR_ENTRY_SIZE),
|
||||
FAT_DIR_ENTRY_SIZE, e);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (e[0] == FAT_DIR_END) {
|
||||
return WOLFBOOT_FS_E_NOENT;
|
||||
}
|
||||
if (e[0] == FAT_DIR_FREE) {
|
||||
memset(&lfn, 0, sizeof(lfn));
|
||||
continue;
|
||||
}
|
||||
|
||||
attr = e[FAT_DIR_ATTR];
|
||||
if ((attr & FAT_ATTR_LFN_MASK) == FAT_ATTR_LFN) {
|
||||
fat32_lfn_accum(&lfn, e);
|
||||
continue;
|
||||
}
|
||||
/* The volume label lives in a directory entry but is not a
|
||||
* name any path can refer to. */
|
||||
if ((attr & FAT_ATTR_VOLUME_ID) != 0U) {
|
||||
memset(&lfn, 0, sizeof(lfn));
|
||||
continue;
|
||||
}
|
||||
|
||||
matched = 0;
|
||||
/* A complete long name set, whose checksum matches this short
|
||||
* entry, is the file's real name. */
|
||||
if ((lfn.have != 0U) && (lfn.expect_ord == 0U) &&
|
||||
(fat32_sfn_checksum(e) == lfn.cksum)) {
|
||||
matched = fat32_name_eq(lfn.name, name);
|
||||
}
|
||||
/* The accumulator must never survive a short entry, or a
|
||||
* discarded set could be paired with the following file. */
|
||||
memset(&lfn, 0, sizeof(lfn));
|
||||
|
||||
if (matched == 0) {
|
||||
fat32_sfn_to_name(e, sfn);
|
||||
matched = fat32_name_eq(sfn, name);
|
||||
}
|
||||
if (matched == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
*out_attr = attr;
|
||||
*out_clus = ((uint32_t)fs_le16(e + FAT_DIR_FST_CLUS_HI) << 16) |
|
||||
(uint32_t)fs_le16(e + FAT_DIR_FST_CLUS_LO);
|
||||
*out_size = fs_le32(e + FAT_DIR_FILE_SIZE);
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
blocks++;
|
||||
ret = fat32_next_clus(vol, clus, &next);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
if (ret == 1) {
|
||||
return WOLFBOOT_FS_E_NOENT;
|
||||
}
|
||||
clus = next;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
* Public backend entry points
|
||||
* --------------------------------------------------------------------- */
|
||||
|
||||
int fat32_open(struct fs_volume *vol, struct fs_file *f, const char *path,
|
||||
uint64_t max_size)
|
||||
{
|
||||
struct fat32_volume *fv = &vol->u.fat;
|
||||
char comp[WOLFBOOT_FS_MAX_NAME + 1];
|
||||
const char *p = path;
|
||||
uint32_t comp_len;
|
||||
uint32_t clus;
|
||||
uint32_t ent_clus = 0;
|
||||
uint32_t ent_size = 0;
|
||||
uint8_t attr = 0;
|
||||
int depth = 0;
|
||||
int have = 0;
|
||||
int ret;
|
||||
|
||||
clus = fv->root_clus;
|
||||
|
||||
for (;;) {
|
||||
ret = fs_path_next(&p, comp, &comp_len, &depth);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
if (have != 0) {
|
||||
/* Descend into whatever the previous component named. */
|
||||
if ((attr & FAT_ATTR_DIRECTORY) == 0U) {
|
||||
return WOLFBOOT_FS_E_NOTFILE;
|
||||
}
|
||||
if (fat32_clus_ok(fv, ent_clus) == 0) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
clus = ent_clus;
|
||||
}
|
||||
ret = fat32_lookup(vol, clus, comp, &ent_clus, &ent_size, &attr);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
have = 1;
|
||||
}
|
||||
|
||||
if (have == 0) {
|
||||
/* The path named the root directory itself. */
|
||||
return WOLFBOOT_FS_E_NOTFILE;
|
||||
}
|
||||
if ((attr & (FAT_ATTR_DIRECTORY | FAT_ATTR_VOLUME_ID)) != 0U) {
|
||||
return WOLFBOOT_FS_E_NOTFILE;
|
||||
}
|
||||
/* A file cannot be larger than the partition holding it. */
|
||||
if ((uint64_t)ent_size > vol->part_sz) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
/* The directory-reported size is attacker-controlled and is about to
|
||||
* bound a load into RAM, so it is checked before any of it is used. */
|
||||
if ((uint64_t)ent_size > max_size) {
|
||||
FAT_DBG("fat32: file size %u exceeds max\r\n", ent_size);
|
||||
return WOLFBOOT_FS_E_TOOBIG;
|
||||
}
|
||||
if (ent_size > 0U) {
|
||||
if (fat32_clus_ok(fv, ent_clus) == 0) {
|
||||
return WOLFBOOT_FS_E_CORRUPT;
|
||||
}
|
||||
}
|
||||
|
||||
ret = fat32_validate_chain(vol, ent_clus, (uint64_t)ent_size);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
f->size = (uint64_t)ent_size;
|
||||
f->first_clus = ent_clus;
|
||||
f->cur_clus = ent_clus;
|
||||
f->cur_off = 0;
|
||||
return WOLFBOOT_FS_OK;
|
||||
}
|
||||
|
||||
int fat32_read(struct fs_file *f, uint64_t off, uint64_t len, uint8_t *buf)
|
||||
{
|
||||
struct fs_volume *vol = f->vol;
|
||||
struct fat32_volume *fv = &vol->u.fat;
|
||||
uint64_t done = 0;
|
||||
uint64_t byte_off, avail, this_len;
|
||||
uint32_t in_clus, run, clus, next;
|
||||
int ret;
|
||||
|
||||
while (done < len) {
|
||||
ret = fat32_seek(f, off + done);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
clus = f->cur_clus;
|
||||
in_clus = (uint32_t)((off + done) % (uint64_t)fv->clus_sz);
|
||||
|
||||
/* Coalesce physically adjacent clusters into a single media read.
|
||||
* An unfragmented file then loads with the same number of disk
|
||||
* transfers as a raw partition would, which matters on the slower
|
||||
* SD targets where a per-cluster read would dominate boot time. */
|
||||
run = 1;
|
||||
while ((((uint64_t)run * fv->clus_sz) - in_clus) < (len - done)) {
|
||||
if (((uint64_t)clus + run) > ((uint64_t)fv->clus_count + 1U)) {
|
||||
break;
|
||||
}
|
||||
ret = fat32_next_clus(vol, clus + run - 1U, &next);
|
||||
if (ret != 0) {
|
||||
/* End of chain, or a bad link. Either way stop extending
|
||||
* the run; a real error is reported by the next seek. */
|
||||
break;
|
||||
}
|
||||
if (next != (clus + run)) {
|
||||
break;
|
||||
}
|
||||
run++;
|
||||
}
|
||||
|
||||
avail = ((uint64_t)run * fv->clus_sz) - in_clus;
|
||||
this_len = len - done;
|
||||
if (this_len > avail) {
|
||||
this_len = avail;
|
||||
}
|
||||
if (this_len > (uint64_t)DISK_IO_MAX_SIZE) {
|
||||
this_len = (uint64_t)DISK_IO_MAX_SIZE;
|
||||
}
|
||||
|
||||
byte_off = fv->data_off +
|
||||
(((uint64_t)clus - 2U) * (uint64_t)fv->clus_sz) + in_clus;
|
||||
/* Defence in depth: mount validated the geometry, so this cannot
|
||||
* fire, but the cost of being wrong here is an out-of-partition
|
||||
* read straight into the RAM load region. */
|
||||
if ((byte_off > vol->part_sz) ||
|
||||
(this_len > (vol->part_sz - byte_off))) {
|
||||
return WOLFBOOT_FS_E_RANGE;
|
||||
}
|
||||
|
||||
ret = fs_disk_read_exact(vol, byte_off, (uint32_t)this_len,
|
||||
buf + done);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
done += this_len;
|
||||
}
|
||||
return (int)done;
|
||||
}
|
||||
|
||||
int fat32_label_eq(struct fs_volume *vol, const char *label)
|
||||
{
|
||||
char vl[FAT_BS_VOL_LAB_LEN + 1];
|
||||
uint8_t raw[FAT_BS_VOL_LAB_LEN];
|
||||
uint32_t n;
|
||||
uint32_t i;
|
||||
int ret;
|
||||
|
||||
ret = fs_meta_read(vol, FAT_BS_VOL_LAB, FAT_BS_VOL_LAB_LEN, raw);
|
||||
if (ret != WOLFBOOT_FS_OK) {
|
||||
return ret;
|
||||
}
|
||||
for (i = 0; i < FAT_BS_VOL_LAB_LEN; i++) {
|
||||
vl[i] = (char)raw[i];
|
||||
}
|
||||
vl[FAT_BS_VOL_LAB_LEN] = '\0';
|
||||
|
||||
/* The label is space-padded to a fixed width on media. */
|
||||
n = FAT_BS_VOL_LAB_LEN;
|
||||
while ((n > 0U) && (vl[n - 1U] == ' ')) {
|
||||
n--;
|
||||
}
|
||||
vl[n] = '\0';
|
||||
|
||||
if (n == 0U) {
|
||||
return 0;
|
||||
}
|
||||
return fat32_name_eq(vl, label);
|
||||
}
|
||||
|
||||
#endif /* WOLFBOOT_DISK_FS && WOLFBOOT_FAT32 */
|
||||
|
||||
#endif /* _WOLFBOOT_FAT32_C_ */
|
||||
|
|
@ -135,6 +135,20 @@ TESTS+=unit-sdhci-acmd41-timeout
|
|||
TESTS+=unit-ti-hercules-write
|
||||
TESTS+=unit-p1021-qe-firmware
|
||||
TESTS+=unit-t10xx-dts-memac
|
||||
TESTS+=unit-fs-probe
|
||||
TESTS+=unit-fs-malicious
|
||||
|
||||
# The filesystem interop tests build their fixtures with the host's real
|
||||
# mkfs tools, so they are only added when those tools exist. The hostile
|
||||
# input coverage lives in unit-fs-malicious, which needs no tools and is
|
||||
# therefore unconditional.
|
||||
HAVE_MKFS_VFAT := $(shell command -v mkfs.vfat 2>/dev/null)
|
||||
HAVE_MCOPY := $(shell command -v mcopy 2>/dev/null)
|
||||
ifneq ($(and $(HAVE_MKFS_VFAT),$(HAVE_MCOPY)),)
|
||||
TESTS+=unit-fat32
|
||||
else
|
||||
$(warning Skipping unit-fat32: mkfs.vfat and/or mtools unavailable)
|
||||
endif
|
||||
|
||||
# The x86-64 EFI unit test needs the gnu-efi development headers (same
|
||||
# dependency as the CMake x86_64_efi target). Probe and only add the test
|
||||
|
|
@ -850,6 +864,48 @@ gpt-sfdisk-test.h:
|
|||
unit-disk: unit-disk.c gpt-sfdisk-test.h
|
||||
gcc -o $@ $< $(CFLAGS) $(LDFLAGS)
|
||||
|
||||
# Backend-independent half of the read-only filesystem layer. Built with
|
||||
# neither WOLFBOOT_FAT32 nor WOLFBOOT_EXT4, so it also pins the behaviour
|
||||
# of a DISK_FS build with no parser selected: everything probes as raw.
|
||||
unit-fs-probe: ../../include/target.h unit-fs-probe.c
|
||||
gcc -o $@ unit-fs-probe.c $(CFLAGS) -DWOLFBOOT_DISK_FS $(LDFLAGS)
|
||||
|
||||
# Hostile-input coverage for every parser bound. Builds all of its
|
||||
# metadata in C, so unlike the interop suites it is never skipped.
|
||||
unit-fs-malicious: ../../include/target.h unit-fs-malicious.c
|
||||
gcc -o $@ unit-fs-malicious.c $(CFLAGS) -DWOLFBOOT_DISK_FS \
|
||||
-DWOLFBOOT_FAT32 $(LDFLAGS)
|
||||
|
||||
# A conformant FAT32 volume needs at least 65525 clusters, so even with
|
||||
# one 512-byte sector per cluster the smallest valid image is about 33 MB.
|
||||
# That is far too large to embed as a C array the way gpt-sfdisk-test.h
|
||||
# does, so the image stays a file and the test reads it through a
|
||||
# file-backed mock disk_read().
|
||||
fat32-test.img:
|
||||
rm -f $@ $@.tmp fat32-payload.bin fat32-nested.bin fat32-short.bin
|
||||
truncate -s 40M $@.tmp
|
||||
mkfs.vfat -F 32 -s 1 -S 512 -n WBTEST $@.tmp >/dev/null
|
||||
head -c 1048576 /dev/urandom > fat32-payload.bin
|
||||
head -c 4096 /dev/urandom > fat32-nested.bin
|
||||
head -c 777 /dev/urandom > fat32-short.bin
|
||||
MTOOLS_SKIP_CHECK=1 mmd -i $@.tmp ::/boot
|
||||
MTOOLS_SKIP_CHECK=1 mmd -i $@.tmp ::/boot/sub
|
||||
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-payload.bin \
|
||||
::/boot/a-very-long-image-name-for-lfn.itb
|
||||
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-nested.bin \
|
||||
::/boot/sub/nested.bin
|
||||
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-short.bin ::/SHORT.BIN
|
||||
# 60 characters, so five LFN entries. Names of 53-64 characters fit
|
||||
# WOLFBOOT_FS_MAX_NAME but need the fifth entry, which is the band a
|
||||
# too-eager ordinal bound silently drops.
|
||||
MTOOLS_SKIP_CHECK=1 mcopy -i $@.tmp fat32-nested.bin \
|
||||
"::/boot/a-deliberately-long-filename-that-needs-five-lfn-entries.itb"
|
||||
mv $@.tmp $@
|
||||
|
||||
unit-fat32: ../../include/target.h unit-fat32.c fat32-test.img
|
||||
gcc -o $@ unit-fat32.c $(CFLAGS) -DWOLFBOOT_DISK_FS -DWOLFBOOT_FAT32 \
|
||||
$(LDFLAGS)
|
||||
|
||||
unit-multiboot: unit-multiboot.c
|
||||
gcc -o $@ unit-multiboot.c $(CFLAGS) $(LDFLAGS)
|
||||
|
||||
|
|
@ -1365,7 +1421,9 @@ GENERATED_SRC:=aurix_erased_extract.h fdt_memrsv_extract.h \
|
|||
t2080_fman_extract.h \
|
||||
ti_hercules_write_extract.h versal_ext_write_extract.h versal_host.c \
|
||||
versal_host.h versal_qspidev_extract.h zynq_erase_extract.h \
|
||||
zynq_write_extract.h
|
||||
zynq_write_extract.h \
|
||||
fat32-test.img fat32-test.img.tmp fat32-payload.bin fat32-nested.bin \
|
||||
fat32-short.bin
|
||||
|
||||
# Tests that are only in $(TESTS) when their toolchain/headers are
|
||||
# present; clean must remove them regardless, or a host that has since
|
||||
|
|
|
|||
|
|
@ -0,0 +1,537 @@
|
|||
/* unit-fat32.c
|
||||
*
|
||||
* Interoperability tests for the read-only FAT32 backend (src/fat32.c)
|
||||
* against a volume produced by the host's real mkfs.vfat and mcopy.
|
||||
*
|
||||
* Crafted and hostile metadata is covered separately in
|
||||
* unit-fs-malicious.c, which needs no external tools and therefore always
|
||||
* runs.
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot 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.
|
||||
*
|
||||
* wolfBoot 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
/* fseek() takes a long, which is 32-bit on some hosts, so offsets past
|
||||
* 2 GiB need fseeko()/off_t even with large-file support enabled. */
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <check.h>
|
||||
|
||||
#include "gpt.c"
|
||||
#include "disk.c"
|
||||
#include "disk_fs.c"
|
||||
#include "fat32.c"
|
||||
|
||||
#define IMG_PATH "fat32-test.img"
|
||||
#define PAYLOAD_PATH "fat32-payload.bin"
|
||||
#define NESTED_PATH "fat32-nested.bin"
|
||||
#define SHORT_PATH "fat32-short.bin"
|
||||
|
||||
#define LFN_NAME "/boot/a-very-long-image-name-for-lfn.itb"
|
||||
/* 60 characters: five LFN entries, still inside WOLFBOOT_FS_MAX_NAME. */
|
||||
#define LFN5_NAME \
|
||||
"/boot/a-deliberately-long-filename-that-needs-five-lfn-entries.itb"
|
||||
|
||||
/* The filesystem starts this far into the fake drive, so every offset the
|
||||
* parser computes has to be partition-relative to land in the right place. */
|
||||
#define PART_PAD (1024ULL * 1024ULL)
|
||||
|
||||
static FILE *img_fp = NULL;
|
||||
static uint64_t img_size = 0;
|
||||
static unsigned int disk_read_count = 0;
|
||||
|
||||
int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
|
||||
{
|
||||
uint64_t off;
|
||||
|
||||
(void)drv;
|
||||
disk_read_count++;
|
||||
if (start < PART_PAD) {
|
||||
if ((start + count) > PART_PAD)
|
||||
return -1;
|
||||
memset(buf, 0, count);
|
||||
return 0;
|
||||
}
|
||||
off = start - PART_PAD;
|
||||
if ((off + count) > img_size)
|
||||
return -1;
|
||||
if (fseeko(img_fp, (off_t)off, SEEK_SET) != 0)
|
||||
return -1;
|
||||
if (fread(buf, 1, (size_t)count, img_fp) != (size_t)count)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int disk_write(int drv, uint64_t start, uint32_t count, const uint8_t *buf)
|
||||
{
|
||||
(void)drv; (void)start; (void)count; (void)buf;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int disk_init(int drv) { (void)drv; return 0; }
|
||||
void disk_close(int drv) { (void)drv; }
|
||||
|
||||
static uint8_t *load_file(const char *path, uint64_t *len)
|
||||
{
|
||||
FILE *fp = fopen(path, "rb");
|
||||
uint8_t *buf;
|
||||
off_t sz;
|
||||
|
||||
ck_assert(fp != NULL);
|
||||
ck_assert_int_eq(fseeko(fp, 0, SEEK_END), 0);
|
||||
sz = ftello(fp);
|
||||
ck_assert(sz > 0);
|
||||
ck_assert_int_eq(fseeko(fp, 0, SEEK_SET), 0);
|
||||
buf = (uint8_t *)malloc((size_t)sz);
|
||||
ck_assert(buf != NULL);
|
||||
ck_assert_uint_eq(fread(buf, 1, (size_t)sz, fp), (size_t)sz);
|
||||
fclose(fp);
|
||||
*len = (uint64_t)sz;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Present the whole test image as partition 0 of drive 0. */
|
||||
static void open_image(void)
|
||||
{
|
||||
if (img_fp == NULL) {
|
||||
img_fp = fopen(IMG_PATH, "rb");
|
||||
ck_assert(img_fp != NULL);
|
||||
ck_assert_int_eq(fseeko(img_fp, 0, SEEK_END), 0);
|
||||
img_size = (uint64_t)ftello(img_fp);
|
||||
ck_assert(img_size > 0);
|
||||
}
|
||||
memset(Drives, 0, sizeof(Drives));
|
||||
Drives[0].drv = 0;
|
||||
Drives[0].is_open = 1;
|
||||
Drives[0].n_parts = 1;
|
||||
Drives[0].part[0].drv = 0;
|
||||
Drives[0].part[0].start = PART_PAD;
|
||||
Drives[0].part[0].end = PART_PAD + img_size - 1;
|
||||
fs_cache_invalidate();
|
||||
disk_read_count = 0;
|
||||
}
|
||||
|
||||
static void mount_ok(struct fs_volume *vol)
|
||||
{
|
||||
open_image();
|
||||
ck_assert_int_eq(fs_mount(vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(vol->type, FS_TYPE_FAT32);
|
||||
}
|
||||
|
||||
/* --- mount --- */
|
||||
|
||||
START_TEST(test_fat32_mount_geometry)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
mount_ok(&vol);
|
||||
/* Below this cluster count the volume would be FAT16 by definition. */
|
||||
ck_assert_uint_ge(vol.u.fat.clus_count, 65525U);
|
||||
ck_assert_uint_eq(vol.u.fat.clus_sz, 512U);
|
||||
ck_assert_uint_ge(vol.u.fat.root_clus, 2U);
|
||||
ck_assert_uint_le(vol.u.fat.root_clus, vol.u.fat.clus_count + 1U);
|
||||
/* The FAT must be able to describe every cluster. */
|
||||
ck_assert(vol.u.fat.fat_sz >= ((uint64_t)vol.u.fat.clus_count + 2U) * 4U);
|
||||
/* The data region must lie inside the partition. */
|
||||
ck_assert(vol.u.fat.data_off +
|
||||
((uint64_t)vol.u.fat.clus_count * vol.u.fat.clus_sz) <= vol.part_sz);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_type_name)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_str_eq(fs_type_name(&vol), "fat32");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- name resolution --- */
|
||||
|
||||
/* A name of 53-64 characters needs five LFN entries but still fits
|
||||
* WOLFBOOT_FS_MAX_NAME (64). Bounding on ord * 13 overestimates the length
|
||||
* by up to 12, which drops this whole band and makes such a BOOT_FILE_A
|
||||
* unresolvable. The per-character bound is what catches genuinely
|
||||
* over-long names. */
|
||||
START_TEST(test_fat32_open_five_entry_long_name)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
open_image();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(vol.type, FS_TYPE_FAT32);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN5_NAME, 1U << 30), WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_eq((unsigned)fs_size(&f), 4096U);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_open_long_name)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)plen);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_open_short_name)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t slen;
|
||||
uint8_t *sdata = load_file(SHORT_PATH, &slen);
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/SHORT.BIN", slen), WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)slen);
|
||||
free(sdata);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* FAT names are case-insensitive, for both the 8.3 and long forms. */
|
||||
START_TEST(test_fat32_open_case_insensitive)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f,
|
||||
"/BOOT/A-VERY-LONG-IMAGE-NAME-FOR-LFN.ITB", 1U << 30),
|
||||
WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/short.bin", 1U << 30),
|
||||
WOLFBOOT_FS_OK);
|
||||
/* The 8.3 alias mkfs generated for the long name. */
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/boot/A-VERY~1.ITB", 1U << 30),
|
||||
WOLFBOOT_FS_OK);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_open_nested_path)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t nlen;
|
||||
uint8_t *ndata = load_file(NESTED_PATH, &nlen);
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/boot/sub/nested.bin", nlen),
|
||||
WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)nlen);
|
||||
free(ndata);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_open_missing)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/boot/nope.itb", 1U << 30),
|
||||
WOLFBOOT_FS_E_NOENT);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/nodir/nope.itb", 1U << 30),
|
||||
WOLFBOOT_FS_E_NOENT);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_open_directory_is_not_a_file)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/boot", 1U << 30),
|
||||
WOLFBOOT_FS_E_NOTFILE);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/", 1U << 30),
|
||||
WOLFBOOT_FS_E_NOTFILE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_cannot_descend_into_a_file)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/SHORT.BIN/x", 1U << 30),
|
||||
WOLFBOOT_FS_E_NOTFILE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* The size in the directory entry is attacker-controlled and is about to
|
||||
* bound a load into RAM, so it must be refused before any of it is used. */
|
||||
START_TEST(test_fat32_open_respects_max_size)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen - 1),
|
||||
WOLFBOOT_FS_E_TOOBIG);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_label)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_label_eq(&vol, "WBTEST"), 1);
|
||||
ck_assert_int_eq(fs_label_eq(&vol, "wbtest"), 1);
|
||||
ck_assert_int_eq(fs_label_eq(&vol, "WBTES"), 0);
|
||||
ck_assert_int_eq(fs_label_eq(&vol, "WBTESTX"), 0);
|
||||
ck_assert_int_eq(fs_label_eq(&vol, ""), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- reading --- */
|
||||
|
||||
START_TEST(test_fat32_read_whole_file)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
uint8_t *got = (uint8_t *)malloc((size_t)plen);
|
||||
|
||||
ck_assert(got != NULL);
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(&f, 0, plen, got), (int)plen);
|
||||
ck_assert_int_eq(memcmp(got, payload, (size_t)plen), 0);
|
||||
free(got);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* The disk loader reads in fixed-size chunks, so the same file must come
|
||||
* back identically when it is fetched piecewise. */
|
||||
START_TEST(test_fat32_read_chunked)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen, off;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
uint8_t *got = (uint8_t *)malloc((size_t)plen);
|
||||
const uint64_t chunk = 4096;
|
||||
int ret;
|
||||
|
||||
ck_assert(got != NULL);
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
for (off = 0; off < plen; off += chunk) {
|
||||
ret = fs_read(&f, off, chunk, got + off);
|
||||
ck_assert_int_gt(ret, 0);
|
||||
ck_assert_uint_eq((unsigned int)ret,
|
||||
(unsigned int)((plen - off) < chunk ? (plen - off) : chunk));
|
||||
}
|
||||
ck_assert_int_eq(memcmp(got, payload, (size_t)plen), 0);
|
||||
free(got);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* Exercise every combination of unaligned start and length around the
|
||||
* cluster boundaries, where the head/tail split happens. */
|
||||
START_TEST(test_fat32_read_cluster_boundaries)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
uint8_t buf[4096];
|
||||
static const int deltas[] = { -3, -1, 0, 1, 3 };
|
||||
uint32_t cs;
|
||||
unsigned int b, d, l;
|
||||
static const uint32_t lens[] = { 1, 7, 511, 512, 513, 1024, 4096 };
|
||||
uint64_t off;
|
||||
int ret;
|
||||
|
||||
mount_ok(&vol);
|
||||
cs = vol.u.fat.clus_sz;
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
|
||||
for (b = 1; b <= 4; b++) {
|
||||
for (d = 0; d < sizeof(deltas) / sizeof(deltas[0]); d++) {
|
||||
off = (uint64_t)((int64_t)((uint64_t)b * cs) + deltas[d]);
|
||||
for (l = 0; l < sizeof(lens) / sizeof(lens[0]); l++) {
|
||||
if ((off + lens[l]) > plen)
|
||||
continue;
|
||||
ret = fs_read(&f, off, lens[l], buf);
|
||||
ck_assert_int_eq(ret, (int)lens[l]);
|
||||
ck_assert_int_eq(memcmp(buf, payload + off, lens[l]), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* A backwards seek must restart the chain walk and still be correct. */
|
||||
START_TEST(test_fat32_read_backwards)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
uint8_t buf[256];
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
|
||||
ck_assert_int_eq(fs_read(&f, plen - 256, 256, buf), 256);
|
||||
ck_assert_int_eq(memcmp(buf, payload + plen - 256, 256), 0);
|
||||
ck_assert_int_eq(fs_read(&f, 0, 256, buf), 256);
|
||||
ck_assert_int_eq(memcmp(buf, payload, 256), 0);
|
||||
ck_assert_int_eq(fs_read(&f, 100000, 256, buf), 256);
|
||||
ck_assert_int_eq(memcmp(buf, payload + 100000, 256), 0);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_read_eof)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
uint8_t buf[512];
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(&f, plen, sizeof(buf), buf), 0);
|
||||
ck_assert_int_eq(fs_read(&f, plen - 10, sizeof(buf), buf), 10);
|
||||
ck_assert_int_eq(memcmp(buf, payload + plen - 10, 10), 0);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_fat32_read_small_files)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t slen, nlen;
|
||||
uint8_t *sdata = load_file(SHORT_PATH, &slen);
|
||||
uint8_t *ndata = load_file(NESTED_PATH, &nlen);
|
||||
uint8_t buf[8192];
|
||||
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/SHORT.BIN", slen), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(&f, 0, slen, buf), (int)slen);
|
||||
ck_assert_int_eq(memcmp(buf, sdata, (size_t)slen), 0);
|
||||
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/boot/sub/nested.bin", nlen),
|
||||
WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(&f, 0, nlen, buf), (int)nlen);
|
||||
ck_assert_int_eq(memcmp(buf, ndata, (size_t)nlen), 0);
|
||||
free(sdata);
|
||||
free(ndata);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* An unfragmented file must coalesce into large media reads rather than
|
||||
* one read per cluster. On the slower SD targets a per-cluster read would
|
||||
* dominate boot time, so this is a performance contract, not a nicety. */
|
||||
START_TEST(test_fat32_contiguous_runs_are_coalesced)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint64_t plen;
|
||||
uint8_t *payload = load_file(PAYLOAD_PATH, &plen);
|
||||
uint8_t *got = (uint8_t *)malloc((size_t)plen);
|
||||
unsigned int reads;
|
||||
unsigned int per_cluster;
|
||||
|
||||
ck_assert(got != NULL);
|
||||
mount_ok(&vol);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, LFN_NAME, plen), WOLFBOOT_FS_OK);
|
||||
|
||||
disk_read_count = 0;
|
||||
ck_assert_int_eq(fs_read(&f, 0, plen, got), (int)plen);
|
||||
reads = disk_read_count;
|
||||
per_cluster = (unsigned int)(plen / vol.u.fat.clus_sz);
|
||||
|
||||
ck_assert_int_eq(memcmp(got, payload, (size_t)plen), 0);
|
||||
/* Far fewer than one media read per cluster: the FAT walk itself costs
|
||||
* a few cached reads, the payload should cost only a handful. */
|
||||
ck_assert_uint_lt(reads, per_cluster / 8U);
|
||||
free(got);
|
||||
free(payload);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
static Suite *wolfboot_suite(void)
|
||||
{
|
||||
Suite *s = suite_create("wolfboot-fat32");
|
||||
TCase *tc_mount = tcase_create("fat32-mount");
|
||||
TCase *tc_name = tcase_create("fat32-names");
|
||||
TCase *tc_read = tcase_create("fat32-read");
|
||||
|
||||
tcase_add_test(tc_mount, test_fat32_mount_geometry);
|
||||
tcase_add_test(tc_mount, test_fat32_type_name);
|
||||
tcase_add_test(tc_mount, test_fat32_label);
|
||||
suite_add_tcase(s, tc_mount);
|
||||
|
||||
tcase_add_test(tc_name, test_fat32_open_long_name);
|
||||
tcase_add_test(tc_name, test_fat32_open_five_entry_long_name);
|
||||
tcase_add_test(tc_name, test_fat32_open_short_name);
|
||||
tcase_add_test(tc_name, test_fat32_open_case_insensitive);
|
||||
tcase_add_test(tc_name, test_fat32_open_nested_path);
|
||||
tcase_add_test(tc_name, test_fat32_open_missing);
|
||||
tcase_add_test(tc_name, test_fat32_open_directory_is_not_a_file);
|
||||
tcase_add_test(tc_name, test_fat32_cannot_descend_into_a_file);
|
||||
tcase_add_test(tc_name, test_fat32_open_respects_max_size);
|
||||
suite_add_tcase(s, tc_name);
|
||||
|
||||
tcase_add_test(tc_read, test_fat32_read_whole_file);
|
||||
tcase_add_test(tc_read, test_fat32_read_chunked);
|
||||
tcase_add_test(tc_read, test_fat32_read_cluster_boundaries);
|
||||
tcase_add_test(tc_read, test_fat32_read_backwards);
|
||||
tcase_add_test(tc_read, test_fat32_read_eof);
|
||||
tcase_add_test(tc_read, test_fat32_read_small_files);
|
||||
tcase_add_test(tc_read, test_fat32_contiguous_runs_are_coalesced);
|
||||
suite_add_tcase(s, tc_read);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fails;
|
||||
Suite *s = wolfboot_suite();
|
||||
SRunner *sr = srunner_create(s);
|
||||
srunner_run_all(sr, CK_NORMAL);
|
||||
fails = srunner_ntests_failed(sr);
|
||||
srunner_free(sr);
|
||||
return fails;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,738 @@
|
|||
/* unit-fs-probe.c
|
||||
*
|
||||
* Unit tests for the backend-independent parts of the read-only
|
||||
* filesystem layer (src/disk_fs.c): filesystem probing, the raw
|
||||
* passthrough, the metadata cache and the path splitter.
|
||||
*
|
||||
* Backend-specific probe rejection (FAT16 geometry, unsupported ext4
|
||||
* features) is covered in unit-fat32.c and unit-ext4.c, where those
|
||||
* parsers are compiled in.
|
||||
*
|
||||
* Copyright (C) 2026 wolfSSL Inc.
|
||||
* This file is part of wolfBoot.
|
||||
*
|
||||
* wolfBoot 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.
|
||||
*
|
||||
* wolfBoot 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <check.h>
|
||||
|
||||
#include "gpt.c"
|
||||
#include "disk.c"
|
||||
#include "disk_fs.c"
|
||||
|
||||
/* Fake disk backing store for mock disk_read/disk_write */
|
||||
#define FAKE_DISK_SIZE (256 * 1024)
|
||||
static uint8_t fake_disk[FAKE_DISK_SIZE];
|
||||
|
||||
/* Set to a byte offset to make disk_read fail at that address. -1 = no fail */
|
||||
static int64_t mock_disk_read_fail_at = -1;
|
||||
|
||||
/* Number of disk_read calls since the last reset */
|
||||
static unsigned int disk_read_count = 0;
|
||||
|
||||
int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf)
|
||||
{
|
||||
(void)drv;
|
||||
disk_read_count++;
|
||||
if (mock_disk_read_fail_at >= 0 && (int64_t)start == mock_disk_read_fail_at)
|
||||
return -1;
|
||||
if (start + count > FAKE_DISK_SIZE)
|
||||
return -1;
|
||||
memcpy(buf, fake_disk + start, count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int disk_write(int drv, uint64_t start, uint32_t count, const uint8_t *buf)
|
||||
{
|
||||
(void)drv;
|
||||
if (start + count > FAKE_DISK_SIZE)
|
||||
return -1;
|
||||
memcpy(fake_disk + start, buf, count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int disk_init(int drv) { (void)drv; return 0; }
|
||||
void disk_close(int drv) { (void)drv; }
|
||||
|
||||
/* Two MBR partitions, in sectors. */
|
||||
#define P0_LBA 8
|
||||
#define P0_SECS 64
|
||||
#define P1_LBA 128
|
||||
#define P1_SECS 64
|
||||
|
||||
#define P0_OFF ((uint64_t)P0_LBA * 512)
|
||||
#define P0_SIZE ((uint64_t)P0_SECS * 512)
|
||||
#define P1_OFF ((uint64_t)P1_LBA * 512)
|
||||
#define P1_SIZE ((uint64_t)P1_SECS * 512)
|
||||
|
||||
static void put_le32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v & 0xFF);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFF);
|
||||
p[2] = (uint8_t)((v >> 16) & 0xFF);
|
||||
p[3] = (uint8_t)((v >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
/* Build an MBR with two type-0x83 partitions and open the drive. */
|
||||
static void build_disk(void)
|
||||
{
|
||||
uint8_t *e;
|
||||
|
||||
memset(fake_disk, 0, sizeof(fake_disk));
|
||||
memset(Drives, 0, sizeof(Drives));
|
||||
|
||||
e = fake_disk + GPT_MBR_ENTRY_START;
|
||||
e[4] = 0x83;
|
||||
put_le32(e + 8, P0_LBA);
|
||||
put_le32(e + 12, P0_SECS);
|
||||
|
||||
e += 16;
|
||||
e[4] = 0x83;
|
||||
put_le32(e + 8, P1_LBA);
|
||||
put_le32(e + 12, P1_SECS);
|
||||
|
||||
fake_disk[GPT_MBR_BOOTSIG_OFFSET] = 0x55;
|
||||
fake_disk[GPT_MBR_BOOTSIG_OFFSET + 1] = 0xAA;
|
||||
|
||||
ck_assert_int_eq(disk_open(0), 2);
|
||||
mock_disk_read_fail_at = -1;
|
||||
disk_read_count = 0;
|
||||
fs_cache_invalidate();
|
||||
}
|
||||
|
||||
/* Fill a partition with a deterministic, offset-dependent pattern. */
|
||||
static void fill_pattern(uint64_t off, uint64_t len, uint8_t seed)
|
||||
{
|
||||
uint64_t i;
|
||||
for (i = 0; i < len; i++)
|
||||
fake_disk[off + i] = (uint8_t)((i * 7U) + seed);
|
||||
}
|
||||
|
||||
/* --- probing --- */
|
||||
|
||||
START_TEST(test_probe_wolfboot_magic_is_raw)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
memcpy(fake_disk + P0_OFF, "WOLF", 4);
|
||||
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
|
||||
ck_assert_uint_eq((unsigned int)vol.part_sz, (unsigned int)P0_SIZE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* A raw image whose header happens to be followed by a 0x55AA at the end
|
||||
* of the sector must still take the raw short-circuit, not be probed. */
|
||||
START_TEST(test_probe_magic_wins_over_boot_signature)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
memcpy(fake_disk + P0_OFF, "WOLF", 4);
|
||||
fake_disk[P0_OFF + 0x1FE] = 0x55;
|
||||
fake_disk[P0_OFF + 0x1FF] = 0xAA;
|
||||
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_probe_empty_partition_is_raw)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* Garbage that is neither an image header nor a filesystem falls back to
|
||||
* raw, exactly as a build without DISK_FS would behave. */
|
||||
START_TEST(test_probe_garbage_is_raw)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x11);
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(vol.type, FS_TYPE_RAW);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_probe_bad_partition_index)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 7), WOLFBOOT_FS_E_IO);
|
||||
ck_assert_int_eq(vol.type, FS_TYPE_NONE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_probe_null_volume)
|
||||
{
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(NULL, 0, 0), WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- raw passthrough --- */
|
||||
|
||||
START_TEST(test_raw_open_size_is_partition_size)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 1), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, NULL, 16), WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_eq((unsigned int)fs_size(&f), (unsigned int)P1_SIZE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_raw_read_matches_disk_part_read)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint8_t via_fs[600];
|
||||
uint8_t via_disk[600];
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x5A);
|
||||
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
|
||||
|
||||
ck_assert_int_eq(fs_read(&f, 300, sizeof(via_fs), via_fs),
|
||||
(int)sizeof(via_fs));
|
||||
ck_assert_int_eq(disk_part_read(0, 0, 300, sizeof(via_disk), via_disk),
|
||||
(int)sizeof(via_disk));
|
||||
ck_assert_int_eq(memcmp(via_fs, via_disk, sizeof(via_fs)), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_raw_read_past_eof_returns_zero)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint8_t buf[16];
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(&f, P0_SIZE, sizeof(buf), buf), 0);
|
||||
ck_assert_int_eq(fs_read(&f, P0_SIZE + 1024, sizeof(buf), buf), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_raw_read_straddling_eof_is_clamped)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint8_t buf[64];
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x33);
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
|
||||
/* Ask for 64 bytes starting 16 before the end: 16 must come back. */
|
||||
ck_assert_int_eq(fs_read(&f, P0_SIZE - 16, sizeof(buf), buf), 16);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_raw_read_zero_length)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
uint8_t buf[4];
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(&f, 0, 0, buf), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_raw_label_never_matches)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_label_eq(&vol, "boot"), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_type_name)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_str_eq(fs_type_name(&vol), "raw");
|
||||
ck_assert_str_eq(fs_type_name(NULL), "none");
|
||||
vol.type = FS_TYPE_NONE;
|
||||
ck_assert_str_eq(fs_type_name(&vol), "none");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- bounded disk access --- */
|
||||
|
||||
/* disk_part_read() silently clamps a read to the partition end and reports
|
||||
* the clamped count. fs_disk_read_exact() must refuse it rather than let a
|
||||
* truncation look like success. */
|
||||
START_TEST(test_read_exact_refuses_partition_end_clamp)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[128];
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
|
||||
/* Straddles the end of the partition. disk_part_read would return 16. */
|
||||
ck_assert_int_eq(fs_disk_read_exact(&vol, P0_SIZE - 16, sizeof(buf), buf),
|
||||
WOLFBOOT_FS_E_RANGE);
|
||||
/* Entirely past the end. */
|
||||
ck_assert_int_eq(fs_disk_read_exact(&vol, P0_SIZE, 4, buf),
|
||||
WOLFBOOT_FS_E_RANGE);
|
||||
/* The last bytes of the partition are still readable. */
|
||||
ck_assert_int_eq(fs_disk_read_exact(&vol, P0_SIZE - 16, 16, buf),
|
||||
WOLFBOOT_FS_OK);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_read_exact_io_error)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[16];
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
mock_disk_read_fail_at = (int64_t)(P0_OFF + 512);
|
||||
ck_assert_int_eq(fs_disk_read_exact(&vol, 512, sizeof(buf), buf),
|
||||
WOLFBOOT_FS_E_IO);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_read_exact_null_and_zero)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[4];
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_disk_read_exact(&vol, 0, 0, buf), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_disk_read_exact(NULL, 0, 4, buf), WOLFBOOT_FS_E_PARAM);
|
||||
ck_assert_int_eq(fs_disk_read_exact(&vol, 0, 4, NULL), WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- metadata cache --- */
|
||||
|
||||
START_TEST(test_meta_read_spans_cache_windows)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[1400];
|
||||
uint64_t i;
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x77);
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
|
||||
/* Unaligned start, length far larger than one cache window. */
|
||||
ck_assert_int_eq(fs_meta_read(&vol, 300, sizeof(buf), buf),
|
||||
WOLFBOOT_FS_OK);
|
||||
for (i = 0; i < sizeof(buf); i++)
|
||||
ck_assert_uint_eq(buf[i], fake_disk[P0_OFF + 300 + i]);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_meta_read_is_cached)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[4];
|
||||
unsigned int first;
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x22);
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
|
||||
/* fs_mount() already warmed the window covering offset 0, so start
|
||||
* from a cold cache to observe the first miss. */
|
||||
fs_cache_invalidate();
|
||||
disk_read_count = 0;
|
||||
ck_assert_int_eq(fs_meta_read(&vol, 16, sizeof(buf), buf), WOLFBOOT_FS_OK);
|
||||
first = disk_read_count;
|
||||
ck_assert_uint_gt(first, 0);
|
||||
|
||||
/* Same window: must not hit the media again. */
|
||||
ck_assert_int_eq(fs_meta_read(&vol, 20, sizeof(buf), buf), WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_eq(disk_read_count, first);
|
||||
|
||||
/* A different window must miss. */
|
||||
ck_assert_int_eq(fs_meta_read(&vol, WOLFBOOT_FS_CACHE_SIZE, sizeof(buf),
|
||||
buf), WOLFBOOT_FS_OK);
|
||||
ck_assert_uint_gt(disk_read_count, first);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* The cache window is keyed by drive and partition as well as by offset.
|
||||
* Slot A and slot B can be different partitions, and an offset-only key
|
||||
* would hand one partition's reads the other partition's bytes. */
|
||||
START_TEST(test_meta_cache_keyed_by_partition)
|
||||
{
|
||||
struct fs_volume v0, v1;
|
||||
uint8_t b0[8], b1[8];
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x01);
|
||||
fill_pattern(P1_OFF, P1_SIZE, 0xF0);
|
||||
|
||||
ck_assert_int_eq(fs_mount(&v0, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_meta_read(&v0, 0, sizeof(b0), b0), WOLFBOOT_FS_OK);
|
||||
|
||||
ck_assert_int_eq(fs_mount(&v1, 0, 1), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_meta_read(&v1, 0, sizeof(b1), b1), WOLFBOOT_FS_OK);
|
||||
|
||||
ck_assert_int_eq(memcmp(b0, fake_disk + P0_OFF, sizeof(b0)), 0);
|
||||
ck_assert_int_eq(memcmp(b1, fake_disk + P1_OFF, sizeof(b1)), 0);
|
||||
ck_assert_int_ne(memcmp(b0, b1, sizeof(b0)), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* A window at the tail of the partition is shorter than the cache size.
|
||||
* The short window must still satisfy reads that fall inside it. */
|
||||
START_TEST(test_meta_read_short_tail_window)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[8];
|
||||
|
||||
build_disk();
|
||||
fill_pattern(P0_OFF, P0_SIZE, 0x44);
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
|
||||
ck_assert_int_eq(fs_meta_read(&vol, P0_SIZE - sizeof(buf), sizeof(buf),
|
||||
buf), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(memcmp(buf, fake_disk + P0_OFF + P0_SIZE - sizeof(buf),
|
||||
sizeof(buf)), 0);
|
||||
ck_assert_int_eq(fs_meta_read(&vol, P0_SIZE - 4, 8, buf),
|
||||
WOLFBOOT_FS_E_RANGE);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_meta_read_out_of_range)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
uint8_t buf[8];
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_meta_read(&vol, P0_SIZE, 8, buf),
|
||||
WOLFBOOT_FS_E_RANGE);
|
||||
ck_assert_int_eq(fs_meta_read(&vol, 0xFFFFFFFFFFFFFF00ULL, 8, buf),
|
||||
WOLFBOOT_FS_E_RANGE);
|
||||
ck_assert_int_eq(fs_meta_read(&vol, 0, 0, buf), WOLFBOOT_FS_OK);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- partition accessors added for the filesystem layer --- */
|
||||
|
||||
START_TEST(test_disk_part_size)
|
||||
{
|
||||
uint64_t sz = 0;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(disk_part_size(0, 0, &sz), 0);
|
||||
ck_assert_uint_eq((unsigned int)sz, (unsigned int)P0_SIZE);
|
||||
ck_assert_int_eq(disk_part_size(0, 1, &sz), 0);
|
||||
ck_assert_uint_eq((unsigned int)sz, (unsigned int)P1_SIZE);
|
||||
|
||||
/* Past the end of the parsed table, and past MAX_PARTITIONS. */
|
||||
ck_assert_int_eq(disk_part_size(0, 2, &sz), -1);
|
||||
ck_assert_int_eq(disk_part_size(0, MAX_PARTITIONS, &sz), -1);
|
||||
ck_assert_int_eq(disk_part_size(0, -1, &sz), -1);
|
||||
ck_assert_int_eq(disk_part_size(MAX_DISKS, 0, &sz), -1);
|
||||
ck_assert_int_eq(disk_part_size(0, 0, NULL), -1);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_disk_part_count)
|
||||
{
|
||||
build_disk();
|
||||
ck_assert_int_eq(disk_part_count(0), 2);
|
||||
|
||||
/* Out-of-range drives, and a drive that was never opened. */
|
||||
ck_assert_int_eq(disk_part_count(-1), -1);
|
||||
ck_assert_int_eq(disk_part_count(MAX_DISKS), -1);
|
||||
memset(Drives, 0, sizeof(Drives));
|
||||
ck_assert_int_eq(disk_part_count(0), -1);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- little-endian accessors --- */
|
||||
|
||||
START_TEST(test_le_accessors)
|
||||
{
|
||||
static const uint8_t b[8] = {
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
|
||||
};
|
||||
|
||||
ck_assert_uint_eq(fs_le16(b), 0x0201U);
|
||||
ck_assert_uint_eq(fs_le32(b), 0x04030201U);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- path splitter --- */
|
||||
|
||||
static int split_all(const char *path, char out[][WOLFBOOT_FS_MAX_NAME + 1],
|
||||
int max)
|
||||
{
|
||||
const char *p = path;
|
||||
char name[WOLFBOOT_FS_MAX_NAME + 1];
|
||||
uint32_t len;
|
||||
int depth = 0;
|
||||
int n = 0;
|
||||
int ret;
|
||||
|
||||
for (;;) {
|
||||
ret = fs_path_next(&p, name, &len, &depth);
|
||||
if (ret <= 0)
|
||||
return (ret < 0) ? ret : n;
|
||||
if (n >= max)
|
||||
return -100;
|
||||
strcpy(out[n], name);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
START_TEST(test_path_split_basic)
|
||||
{
|
||||
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
|
||||
ck_assert_int_eq(split_all("/boot/image.itb", parts, 4), 2);
|
||||
ck_assert_str_eq(parts[0], "boot");
|
||||
ck_assert_str_eq(parts[1], "image.itb");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_path_split_collapses_slashes)
|
||||
{
|
||||
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
|
||||
ck_assert_int_eq(split_all("//boot///image.itb//", parts, 4), 2);
|
||||
ck_assert_str_eq(parts[0], "boot");
|
||||
ck_assert_str_eq(parts[1], "image.itb");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* A backslash is an ordinary filename character on both FAT and ext4.
|
||||
* Treating it as a separator would let a crafted name split in two. */
|
||||
START_TEST(test_path_backslash_is_not_a_separator)
|
||||
{
|
||||
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
|
||||
ck_assert_int_eq(split_all("/a\\b", parts, 4), 1);
|
||||
ck_assert_str_eq(parts[0], "a\\b");
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_path_rejects_dot_and_dotdot)
|
||||
{
|
||||
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
|
||||
ck_assert_int_eq(split_all("/boot/./image", parts, 4),
|
||||
WOLFBOOT_FS_E_PARAM);
|
||||
ck_assert_int_eq(split_all("/boot/../image", parts, 4),
|
||||
WOLFBOOT_FS_E_PARAM);
|
||||
ck_assert_int_eq(split_all("/..", parts, 4), WOLFBOOT_FS_E_PARAM);
|
||||
/* Names that merely begin with a dot are fine. */
|
||||
ck_assert_int_eq(split_all("/.config", parts, 4), 1);
|
||||
ck_assert_int_eq(split_all("/...", parts, 4), 1);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_path_rejects_excess_depth)
|
||||
{
|
||||
char parts[16][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
char path[WOLFBOOT_FS_MAX_PATH + 8];
|
||||
int i;
|
||||
|
||||
path[0] = '\0';
|
||||
for (i = 0; i < WOLFBOOT_FS_MAX_PATH_DEPTH; i++)
|
||||
strcat(path, "/a");
|
||||
ck_assert_int_eq(split_all(path, parts, 16), WOLFBOOT_FS_MAX_PATH_DEPTH);
|
||||
|
||||
strcat(path, "/a");
|
||||
ck_assert_int_eq(split_all(path, parts, 16), WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_path_rejects_long_component)
|
||||
{
|
||||
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
char path[WOLFBOOT_FS_MAX_NAME + 8];
|
||||
int i;
|
||||
|
||||
path[0] = '/';
|
||||
for (i = 0; i < WOLFBOOT_FS_MAX_NAME; i++)
|
||||
path[1 + i] = 'x';
|
||||
path[1 + WOLFBOOT_FS_MAX_NAME] = '\0';
|
||||
ck_assert_int_eq(split_all(path, parts, 4), 1);
|
||||
|
||||
path[1 + WOLFBOOT_FS_MAX_NAME] = 'x';
|
||||
path[2 + WOLFBOOT_FS_MAX_NAME] = '\0';
|
||||
ck_assert_int_eq(split_all(path, parts, 4), WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_path_empty)
|
||||
{
|
||||
char parts[4][WOLFBOOT_FS_MAX_NAME + 1];
|
||||
|
||||
ck_assert_int_eq(split_all("/", parts, 4), 0);
|
||||
ck_assert_int_eq(split_all("", parts, 4), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_path_next_null_args)
|
||||
{
|
||||
char name[WOLFBOOT_FS_MAX_NAME + 1];
|
||||
uint32_t len;
|
||||
int depth = 0;
|
||||
|
||||
ck_assert_int_eq(fs_path_next(NULL, name, &len, &depth),
|
||||
WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
/* --- fs_open argument validation --- */
|
||||
|
||||
/* With no backend compiled in, every partition probes as raw, so these
|
||||
* checks are exercised against a synthetic non-raw type. */
|
||||
START_TEST(test_open_rejects_bad_type)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
vol.type = FS_TYPE_NONE;
|
||||
ck_assert_int_eq(fs_open(&vol, &f, "/boot/x", 1024),
|
||||
WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_open_null_args)
|
||||
{
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(NULL, &f, "/x", 16), WOLFBOOT_FS_E_PARAM);
|
||||
ck_assert_int_eq(fs_open(&vol, NULL, "/x", 16), WOLFBOOT_FS_E_PARAM);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
START_TEST(test_read_null_args)
|
||||
{
|
||||
uint8_t buf[4];
|
||||
struct fs_volume vol;
|
||||
struct fs_file f;
|
||||
|
||||
build_disk();
|
||||
ck_assert_int_eq(fs_mount(&vol, 0, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_open(&vol, &f, NULL, 0), WOLFBOOT_FS_OK);
|
||||
ck_assert_int_eq(fs_read(NULL, 0, 4, buf), WOLFBOOT_FS_E_PARAM);
|
||||
ck_assert_int_eq(fs_read(&f, 0, 4, NULL), WOLFBOOT_FS_E_PARAM);
|
||||
ck_assert_uint_eq((unsigned int)fs_size(NULL), 0);
|
||||
}
|
||||
END_TEST
|
||||
|
||||
static Suite *wolfboot_suite(void)
|
||||
{
|
||||
Suite *s = suite_create("wolfboot-fs-probe");
|
||||
TCase *tc_probe = tcase_create("probe");
|
||||
TCase *tc_raw = tcase_create("raw");
|
||||
TCase *tc_io = tcase_create("bounded-io");
|
||||
TCase *tc_cache = tcase_create("cache");
|
||||
TCase *tc_path = tcase_create("path");
|
||||
TCase *tc_cov = tcase_create("fs-coverage");
|
||||
|
||||
tcase_add_test(tc_probe, test_probe_wolfboot_magic_is_raw);
|
||||
tcase_add_test(tc_probe, test_probe_magic_wins_over_boot_signature);
|
||||
tcase_add_test(tc_probe, test_probe_empty_partition_is_raw);
|
||||
tcase_add_test(tc_probe, test_probe_garbage_is_raw);
|
||||
tcase_add_test(tc_probe, test_probe_bad_partition_index);
|
||||
tcase_add_test(tc_probe, test_probe_null_volume);
|
||||
suite_add_tcase(s, tc_probe);
|
||||
|
||||
tcase_add_test(tc_raw, test_raw_open_size_is_partition_size);
|
||||
tcase_add_test(tc_raw, test_raw_read_matches_disk_part_read);
|
||||
tcase_add_test(tc_raw, test_raw_read_past_eof_returns_zero);
|
||||
tcase_add_test(tc_raw, test_raw_read_straddling_eof_is_clamped);
|
||||
tcase_add_test(tc_raw, test_raw_read_zero_length);
|
||||
tcase_add_test(tc_raw, test_raw_label_never_matches);
|
||||
tcase_add_test(tc_raw, test_type_name);
|
||||
suite_add_tcase(s, tc_raw);
|
||||
|
||||
tcase_add_test(tc_io, test_read_exact_refuses_partition_end_clamp);
|
||||
tcase_add_test(tc_io, test_read_exact_io_error);
|
||||
tcase_add_test(tc_io, test_read_exact_null_and_zero);
|
||||
suite_add_tcase(s, tc_io);
|
||||
|
||||
tcase_add_test(tc_cache, test_meta_read_spans_cache_windows);
|
||||
tcase_add_test(tc_cache, test_meta_read_is_cached);
|
||||
tcase_add_test(tc_cache, test_meta_cache_keyed_by_partition);
|
||||
tcase_add_test(tc_cache, test_meta_read_short_tail_window);
|
||||
tcase_add_test(tc_cache, test_meta_read_out_of_range);
|
||||
suite_add_tcase(s, tc_cache);
|
||||
|
||||
tcase_add_test(tc_path, test_path_split_basic);
|
||||
tcase_add_test(tc_path, test_path_split_collapses_slashes);
|
||||
tcase_add_test(tc_path, test_path_backslash_is_not_a_separator);
|
||||
tcase_add_test(tc_path, test_path_rejects_dot_and_dotdot);
|
||||
tcase_add_test(tc_path, test_path_rejects_excess_depth);
|
||||
tcase_add_test(tc_path, test_path_rejects_long_component);
|
||||
tcase_add_test(tc_path, test_path_empty);
|
||||
tcase_add_test(tc_path, test_path_next_null_args);
|
||||
suite_add_tcase(s, tc_path);
|
||||
|
||||
tcase_add_test(tc_cov, test_disk_part_size);
|
||||
tcase_add_test(tc_cov, test_disk_part_count);
|
||||
tcase_add_test(tc_cov, test_le_accessors);
|
||||
tcase_add_test(tc_cov, test_open_rejects_bad_type);
|
||||
tcase_add_test(tc_cov, test_open_null_args);
|
||||
tcase_add_test(tc_cov, test_read_null_args);
|
||||
suite_add_tcase(s, tc_cov);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fails;
|
||||
Suite *s = wolfboot_suite();
|
||||
SRunner *sr = srunner_create(s);
|
||||
srunner_run_all(sr, CK_NORMAL);
|
||||
fails = srunner_ntests_failed(sr);
|
||||
srunner_free(sr);
|
||||
return fails;
|
||||
}
|
||||
Loading…
Reference in New Issue