diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74afa07..45a92df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,57 @@ jobs: test -f build/dmf/dmblkid.dmf test -f build/dmf/dmblkid_version.txt test -f build/dmf/test_dmblkid.dmf + test -f build/dmf/blkid.dmf + test -f build/dmf/dmpart.dmf + test -f build/dmf/test_dmpart.dmf echo "Module files present" + # dmblkid uses the 64-bit file API (Dmod_FileSeek/_FileTell/_FileSize + # 2.0, dmod#312). The dmod_loader in the 1.0.4 image predates it, so + # the tests run in one built from the same dmod develop the module + # is built against. + - name: Build dmod_loader from dmod develop + run: | + git clone --depth 1 -b develop https://github.com/choco-technologies/dmod.git /tmp/dmod + cmake -S /tmp/dmod -B /tmp/dmod/build -DDMOD_MODE=DMOD_SYSTEM \ + -DDMOD_BUILD_EXAMPLES=ON -DDMOD_BUILD_TESTS=OFF + cmake --build /tmp/dmod/build --target dmod_loader + echo "DMOD_LOADER=/tmp/dmod/build/examples/system/dmod_loader/dmod_loader" >> $GITHUB_ENV + - name: Run tests with dmod_loader run: | export DMOD_DMF_DIR=$(pwd)/build/dmf + dmf-get install -d ${DMOD_DMF_DIR}/test_dmpart-local.dmd -y + ${DMOD_LOADER} build/dmf/test_dmpart.dmf dmf-get install -d ${DMOD_DMF_DIR}/test_dmblkid-local.dmd -y - dmod_loader build/dmf/test_dmblkid.dmf + ${DMOD_LOADER} build/dmf/test_dmblkid.dmf + + build-targets: + name: Build for ${{ matrix.arch }} + runs-on: ubuntu-latest + permissions: + contents: read + container: + image: chocotechnologies/dmod:1.0.4 + strategy: + matrix: + arch: [ armv7/cortex-m4, armv7/cortex-m7 ] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build dmblkid, dmpart and blkid + run: | + mkdir -p build + cd build + cmake .. -DDMOD_TOOLS_NAME=arch/${{ matrix.arch }} + cmake --build . > build.log 2>&1 || { cat build.log; exit 1; } + cat build.log + if grep -E "warning:|error:" build.log | grep -v "build-id"; then + echo "Compiler warnings found" + exit 1 + fi + test -f dmf/dmblkid.dmf + test -f dmf/blkid.dmf + test -f dmf/dmpart.dmf diff --git a/.gitignore b/.gitignore index c2af1f8..3d71ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ build/ .idea/ # Temporary files +dmblkid_test.img *.tmp *.swp *~ diff --git a/CMakeLists.txt b/CMakeLists.txt index c0334ed..855475b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ set(DMOD_BUILD_TEMPLATES OFF CACHE BOOL "Build templates") FetchContent_MakeAvailable(dmod) project(dmblkid VERSION ${DMOD_MODULE_VERSION} - DESCRIPTION "dmblkid DMOD library module" + DESCRIPTION "Block device content (filesystem / partition table) probing" LANGUAGES C CXX) set(DMOD_DIR ${dmod_SOURCE_DIR} CACHE PATH "DMOD source directory") @@ -60,8 +60,13 @@ set(DMOD_DIF_IMPLS ) # (for example, to link libraries) # dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} - # List of source files - can include C and C++ files src/dmblkid.c + src/dmblkid_source.c + src/dmblkid_util.c + src/dmblkid_ptable.c + src/dmblkid_fat.c + src/dmblkid_exfat.c + src/dmblkid_dmffs.c ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE @@ -72,9 +77,19 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) -# To depend on another module's headers, use: -# dmod_link_modules(${DMOD_MODULE_NAME} -# other_module@1.0 -# ) +# dmdrvi: DMDRVI_IOCTL_BLOCK_GET_INFO, the geometry of dmdevfs block nodes +dmod_link_modules(${DMOD_MODULE_NAME} + dmdrvi@>=2.0 +) + +# dmpart - the partition table parser, a separate library module of this +# repository (dmdevfs uses it too). Local sibling: its headers come from its +# _if target rather than a dmod_link_modules() fetch. +target_link_libraries(${DMOD_MODULE_NAME} dmpart_if) +add_subdirectory(libs/dmpart) + +# blkid - command line front end +add_subdirectory(services/blkid) + enable_testing() add_subdirectory(tests) diff --git a/Makefile b/Makefile index 788e8cc..3c5fd03 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # ############################################################################# -# -# This is an example of a simple library module. +# +# dmblkid - block device content probing library. # # ############################################################################# DMOD_DIR=@DMOD_DIR@ @@ -24,13 +24,14 @@ DMOD_MODULE_VERSION=0.1 DMOD_AUTHOR_NAME=Patryk Kubiak # The list of C sources -DMOD_CSOURCES=src/dmblkid.c +DMOD_CSOURCES=src/dmblkid.c src/dmblkid_source.c src/dmblkid_util.c src/dmblkid_ptable.c \ + src/dmblkid_fat.c src/dmblkid_exfat.c src/dmblkid_dmffs.c # The list of C++ sources DMOD_CXXSOURCES= # The list of include directories -DMOD_INC_DIRS=include +DMOD_INC_DIRS=include libs/dmpart/include # The list of libraries to link DMOD_LIBS= diff --git a/README.md b/README.md index 60046e5..4380a81 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,110 @@ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![CI](https://github.com/choco-technologies/dmblkid/actions/workflows/ci.yml/badge.svg)](https://github.com/choco-technologies/dmblkid/actions/workflows/ci.yml) -dmblkid DMOD library module. +Identifies what a block device or partition contains - the DMOD counterpart +of `libblkid`. Automount uses it to pick the filesystem module for a node +(and to leave whole disks with partitions alone); the `blkid` application +prints it. ## Description -TODO: describe what this module does. +`dmblkid_probe()` reads a node such as `/dev/dmsdio0/0` or +`/dev/dmsdio0/0p1` (or an image file) through the regular file API with +64-bit offsets and reports one of: + +| Found | `usage` | `type` | Also reported | +|-------|---------|--------|---------------| +| MBR (incl. extended/logical partitions) | `partition_table` | `mbr` | disk signature, partition count, block size | +| GPT (primary or backup header) | `partition_table` | `gpt` | disk GUID, partition count, block size | +| FAT12/16/32 | `filesystem` | `vfat` | `FAT12`/`FAT16`/`FAT32`, label, serial | +| exFAT | `filesystem` | `exfat` | revision, label, serial | +| dmffs image | `filesystem` | `dmffs` | VERSION value | +| blank, damaged or unsupported | `unknown` | - | size | + +Every type is mapped to the DMOD filesystem module that handles it and +whether that module can mount the node. Today none can: FAT and exFAT have +no module yet, and dmffs mounts memory-mapped flash rather than a node - so +they are reported, but marked not mountable. + +Detection is strict, so that nothing is mounted by mistake: + +- **FAT** - the whole BPB must be consistent, not only the `0x55AA` marker: + jump instruction, sector and cluster sizes, reserved sectors, FAT count, + media byte, regions inside the volume, FATs large enough for every cluster, + volume inside the node. The FAT type comes from the cluster count, as in + the Microsoft specification. +- **exFAT** - boot sector fields, region layout and the boot region checksum. +- **dmffs** - no magic number: the top-level TLV chain must be well formed. +- **MBR/GPT** - [dmpart](libs/dmpart/README.md), the partition table + parser library of this repository, which dmdevfs uses to create partition + nodes too: a disk dmdevfs splits into partitions is always reported as a + partition table. A FAT/exFAT boot sector (a partitionless "superfloppy") is never + taken for an MBR, even if its boot code looks like partition entries. GPT + headers and entry arrays are CRC-checked; the backup header is used if the + primary one is damaged. + +Probing never writes: the node is opened read-only. The block size comes from +`DMDRVI_IOCTL_BLOCK_GET_INFO` when the node answers it (dmdevfs block and +partition nodes); otherwise it is 512, or the size at which a GPT header is +found (1024/2048/4096). A node without a size (not a block device) is +reported as unknown without being read. + +## Usage + +```c +#include "dmblkid.h" + +dmblkid_t* result = NULL; +if (dmblkid_probe("/dev/dmsdio0/0p1", &result) == 0) +{ + if (dmblkid_get_usage(result) == dmblkid_usage_filesystem && dmblkid_is_mountable(result)) + { + mount(dmblkid_get_module(result), ...); + } + dmblkid_destroy(result); +} +``` + +The command line front end: + +``` +> blkid /dev/dmsdio0/0 /dev/dmsdio0/0p1 +/dev/dmsdio0/0: PTTYPE="mbr" PTUUID="1a2b3c4d" PARTITIONS="1" BLOCK_SIZE="512" SIZE="15931539456" +/dev/dmsdio0/0p1: TYPE="vfat" VERSION="FAT32" LABEL="SDCARD" UUID="CAFE-BABE" SIZE="15930490880" MOUNTABLE="no" +``` + +See [services/blkid/README.md](services/blkid/README.md). + +## API + +| Function | Description | +|----------|-------------| +| `dmblkid_probe()` | Identify the contents of a node; returns a result to query. | +| `dmblkid_destroy()` | Release a result. | +| `dmblkid_get_usage()` | Filesystem, partition table or unknown. | +| `dmblkid_get_type()` | `vfat`, `exfat`, `dmffs`, `mbr`, `gpt`. | +| `dmblkid_get_version()` | `FAT32`, exFAT revision, dmffs VERSION. | +| `dmblkid_get_label()` | Volume label. | +| `dmblkid_get_uuid()` | Serial, MBR disk signature or GPT disk GUID. | +| `dmblkid_get_module()` | DMOD filesystem module for the type. | +| `dmblkid_is_mountable()` | Whether that module can mount the node. | +| `dmblkid_get_size()` | Node size in bytes. | +| `dmblkid_get_block_size()` | Block size the partition table was read with. | +| `dmblkid_get_partition_count()` | Number of partitions in the table. | + +The MBR/GPT parser itself is the separate [dmpart](libs/dmpart/README.md) +library module (`dmpart_scan()`), built and released with dmblkid. + +See [include/dmblkid.h](include/dmblkid.h) and +[docs/api-reference.md](docs/api-reference.md). + +## Requirements + +dmblkid uses the 64-bit file API (`Dmod_FileSeek`/`Dmod_FileTell`/ +`Dmod_FileSize` 2.0, dmod#312), so the system it runs in - the firmware or +`dmod_loader` - must be built from a dmod with those APIs. The +`dmod_loader` in the `chocotechnologies/dmod:1.0.4` image predates them; CI +builds one from dmod `develop` (see below). ## Building @@ -21,7 +120,8 @@ cmake --build . ``` Pass `-DDMOD_DIR=/path/to/local/dmod` to build against a local dmod checkout -instead of fetching `develop` from GitHub. +instead of fetching `develop` from GitHub, or +`-DDMOD_TOOLS_NAME=arch/armv7/cortex-m7` to build for a target. ### Using Make @@ -31,44 +131,30 @@ make DMOD_MODE=DMOD_MODULE DMOD_DIR=/path/to/dmod ## Testing -Tests are built automatically alongside the module (see `tests/`). Once built, -run them with `ctest`: +The tests build disk images (sparse files, up to 6 GiB) and probe them: +FAT12/16/32, exFAT with its label above 4 GiB, a damaged exFAT boot region, +dmffs, an MBR with a logical partition, GPT with 512 and 4096-byte blocks, +GPT with only the backup header valid at the end of a 6 GiB disk, a blank +device, a file too small for any format, damaged FAT boot sectors, and that +probing leaves the image unchanged. -```bash -cd build -ctest --output-on-failure -``` - -`ctest` installs the test module's dependencies with `dmf-get` and then runs -it through `dmod_loader`. To run it manually instead: +They need a `dmod_loader` with the 64-bit file API: ```bash +git clone --depth 1 -b develop https://github.com/choco-technologies/dmod.git /tmp/dmod +cmake -S /tmp/dmod -B /tmp/dmod/build -DDMOD_MODE=DMOD_SYSTEM -DDMOD_BUILD_EXAMPLES=ON -DDMOD_BUILD_TESTS=OFF +cmake --build /tmp/dmod/build --target dmod_loader + export DMOD_DMF_DIR=$(pwd)/build/dmf dmf-get install -d ${DMOD_DMF_DIR}/test_dmblkid-local.dmd -y -dmod_loader build/dmf/test_dmblkid.dmf -``` - -## Usage - - - -This library module provides functions that can be used by other modules: +/tmp/dmod/build/examples/system/dmod_loader/dmod_loader build/dmf/test_dmblkid.dmf -```c -#include "dmblkid.h" +dmf-get install -d ${DMOD_DMF_DIR}/test_dmpart-local.dmd -y +/tmp/dmod/build/examples/system/dmod_loader/dmod_loader build/dmf/test_dmpart.dmf ``` -## API - -| Function | Description | -|----------|-------------| -| `dmblkid_create()` | Create a new `dmblkid_t` instance. | -| `dmblkid_destroy()` | Destroy an instance created by `_create()`. | -| `dmblkid_is_valid()` | Check whether a handle is a valid instance. | - -See [include/dmblkid.h](include/dmblkid.h) for the full -declarations and [docs/api-reference.md](docs/api-reference.md) for the -complete reference. +`ctest` runs the same; point it to that loader with +`-DDMOD_LOADER_EXECUTABLE=/tmp/dmod/build/examples/system/dmod_loader/dmod_loader`. ## Documentation @@ -77,18 +163,31 @@ See the `docs/` directory: - **[api-reference.md](docs/api-reference.md)** - Complete API documentation View documentation using `dmf-man dmblkid`. + ## Project Structure ``` dmblkid/ -├── docs/ # Documentation (markdown format) -├── include/ # Public headers -│ └── dmblkid.h +├── docs/ # Documentation (markdown format) +├── include/ +│ └── dmblkid.h # Public API +├── libs/ +│ └── dmpart/ # dmpart - MBR/GPT parser library (also used by dmdevfs) +├── services/ +│ └── blkid/ # blkid command line application ├── src/ -│ └── dmblkid.c +│ ├── dmblkid.c # dmblkid_probe(), result accessors +│ ├── dmblkid_internal.h +│ ├── dmblkid_source.c # Node access (file API, geometry) +│ ├── dmblkid_util.c # Result/string helpers +│ ├── dmblkid_ptable.c # Partition tables (through dmpart) +│ ├── dmblkid_fat.c # FAT12/16/32 +│ ├── dmblkid_exfat.c # exFAT +│ └── dmblkid_dmffs.c # dmffs ├── tests/ │ ├── CMakeLists.txt -│ └── dmblkid_test.c +│ ├── dmblkid_test.c +│ └── test_images.c # Image builders ├── CMakeLists.txt ├── Makefile ├── dmblkid.dmr diff --git a/docs/README.md b/docs/README.md index 047075e..673d2e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,8 @@ # dmblkid Documentation -Welcome to the dmblkid module documentation. +dmblkid identifies what a block device or partition contains: a partition +table (MBR/GPT) or a filesystem (FAT12/16/32, exFAT, dmffs), with its label, +uuid and the DMOD filesystem module that mounts it. ## Contents @@ -10,8 +12,17 @@ Welcome to the dmblkid module documentation. ```c #include "dmblkid.h" + +dmblkid_t* result = NULL; +if (dmblkid_probe("/dev/dmsdio0/0p1", &result) == 0) +{ + const char* type = dmblkid_get_type(result); /* "vfat", "exfat", "dmffs", "mbr", "gpt" or NULL */ + dmblkid_destroy(result); +} ``` +From the shell: `blkid ...` (the `blkid` application module). + View documentation using `dmf-man`: ```bash diff --git a/docs/api-reference.md b/docs/api-reference.md index c4d2f40..6b70f95 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,12 +1,169 @@ # dmblkid API Reference -Document the module's public types and functions here as you add them to -`include/dmblkid.h`. +```c +#include "dmblkid.h" +``` ## Types -_(none yet)_ +### `dmblkid_t` + +Opaque probe result, created by `dmblkid_probe()` and released with +`dmblkid_destroy()`. Every accessor checks that it gets a valid result and +returns the "nothing" value (`NULL`, `0`, `false`, `dmblkid_usage_unknown`) +otherwise. + +### `dmblkid_usage_t` + +| Value | Meaning | +|-------|---------| +| `dmblkid_usage_unknown` | Nothing recognized: blank, damaged or unsupported contents. | +| `dmblkid_usage_filesystem` | A filesystem. | +| `dmblkid_usage_partition_table` | An MBR or GPT. Its partitions are separate nodes (dmdevfs `p`); the whole node must not be mounted. | ## Functions -_(none yet)_ +### `dmblkid_probe` + +```c +int dmblkid_probe(const char* node_path, dmblkid_t** result); +``` + +Opens `node_path` read-only and identifies its contents. Probe order: + +1. partition table (MBR, GPT), +2. FAT12/16/32, exFAT, +3. dmffs. + +The first match wins. + +Geometry: `DMDRVI_IOCTL_BLOCK_GET_INFO` if the node answers it, otherwise the +file size with a 512-byte block size (or 1024/2048/4096 if a GPT header is +found at that block size). A node without a size - not a block device (a +UART would block the first read), or an empty file - is reported as unknown +without being read. + +Returns: + +| Value | Meaning | +|-------|---------| +| `0` | Success. `*result` is set, also when nothing was recognized. | +| `-EINVAL` | `node_path` or `result` is `NULL`. | +| `-ENOENT` | The node cannot be opened. | +| `-EIO` | Reading the node failed. | +| `-ENOMEM` | Out of memory. | + +A structure that would lie beyond the end of the node is treated as "not +this format", not as an error. + +### `dmblkid_destroy` + +```c +void dmblkid_destroy(dmblkid_t* result); +``` + +Releases a result. `NULL` and invalid pointers are ignored. + +### `dmblkid_get_usage` + +```c +dmblkid_usage_t dmblkid_get_usage(const dmblkid_t* result); +``` + +### `dmblkid_get_type` + +```c +const char* dmblkid_get_type(const dmblkid_t* result); +``` + +`"vfat"`, `"exfat"`, `"dmffs"`, `"mbr"`, `"gpt"`, or `NULL` if nothing was +recognized. + +### `dmblkid_get_version` + +```c +const char* dmblkid_get_version(const dmblkid_t* result); +``` + +| Type | Version | +|------|---------| +| `vfat` | `"FAT12"`, `"FAT16"` or `"FAT32"` | +| `exfat` | FileSystemRevision, e.g. `"1.0"` | +| `dmffs` | The VERSION TLV value (e.g. `"1.0"`), `NULL` if the image has none | + +### `dmblkid_get_label` + +```c +const char* dmblkid_get_label(const dmblkid_t* result); +``` + +The volume label, or `NULL` if there is none. + +- **FAT**: the volume label entry of the root directory (the label the OS + shows). Otherwise the extended BPB label; `"NO NAME"` means no label. + Trailing spaces are removed; non-ASCII bytes become `?`. +- **exFAT**: the volume label entry of the first root directory cluster, + converted to UTF-8. + +### `dmblkid_get_uuid` + +```c +const char* dmblkid_get_uuid(const dmblkid_t* result); +``` + +| Type | Format | +|------|--------| +| `vfat`, `exfat` | Volume serial, `"ABCD-1234"` | +| `mbr` | Disk signature, `"1a2b3c4d"` (`NULL` if zero) | +| `gpt` | Disk GUID, `"01234567-89ab-cdef-0011-223344556677"` | + +### `dmblkid_get_module` + +```c +const char* dmblkid_get_module(const dmblkid_t* result); +``` + +The DMOD filesystem module that handles the type: `"dmffs"` for dmffs. +`NULL` for types the ecosystem has no module for yet (FAT, exFAT), for +partition tables and for unknown contents. + +### `dmblkid_is_mountable` + +```c +bool dmblkid_is_mountable(const dmblkid_t* result); +``` + +`true` only if `dmblkid_get_module()` can mount the node itself. `false` for +dmffs: its module mounts memory-mapped flash (`flash_addr`/`flash_size`), not +a node. + +### `dmblkid_get_size` + +```c +uint64_t dmblkid_get_size(const dmblkid_t* result); +``` + +Size of the node in bytes, `0` if unknown. + +### `dmblkid_get_block_size` + +```c +uint32_t dmblkid_get_block_size(const dmblkid_t* result); +``` + +The logical block size the partition table was read with (partition tables +only, otherwise `0`). + +### `dmblkid_get_partition_count` + +```c +uint32_t dmblkid_get_partition_count(const dmblkid_t* result); +``` + +The number of partitions in the table. The extended partition container of +an MBR is not counted; its logical partitions are. + +## Partition tables + +The MBR/GPT parser is the separate `dmpart` library module +(`libs/dmpart`, `dmpart_scan()`), shared with dmdevfs - see its README. diff --git a/include/dmblkid.h b/include/dmblkid.h index be4e6f2..4a46a23 100644 --- a/include/dmblkid.h +++ b/include/dmblkid.h @@ -8,44 +8,93 @@ #include "dmblkid_defs.h" /** - * Public API for the dmblkid module. + * dmblkid - identifies what a block device or partition contains. * - * Functions are declared with the dmod_dmblkid_api(...) macro - dmod's - * standard pattern for functions callable from other modules (or from this - * module's own tests/), resolved dynamically by the loader rather than - * through normal static linkage. See dm_sw_ring/include/dm_sw_ring.h for a - * fully worked real-world example of the same shape. + * dmblkid_probe() reads a node (e.g. /dev/dmsdio0/0 or /dev/dmsdio0/0p1) + * through the regular file API with 64-bit offsets and reports: + * - a partition table (MBR/GPT), so a whole disk with partitions is never + * mounted, or + * - a filesystem (FAT12/16/32, exFAT, dmffs) with its label, uuid and the + * DMOD filesystem module that mounts it, or + * - nothing recognized (blank or damaged medium, unsupported format). * - * Definitions in src/dmblkid.c use the matching - * dmod_dmblkid_api_declaration(...) macro - a plain C function - * definition here will NOT satisfy these declarations at link time. - * - * This is an example interface using the usual "opaque handle" pattern - - * replace the handle, functions, and struct definition in - * src/dmblkid.c with your module's real API. + * Probing only ever reads: the node is opened read-only. */ -/* Opaque handle - the real struct is defined in src/dmblkid.c */ -typedef struct dmblkid* dmblkid_t; +/** Opaque probe result - see dmblkid_probe(). */ +typedef struct dmblkid dmblkid_t; + +/** What was found on the node. */ +typedef enum +{ + dmblkid_usage_unknown = 0, /**< Nothing recognized (blank, damaged or unsupported) */ + dmblkid_usage_filesystem, /**< A filesystem - see dmblkid_get_type() */ + dmblkid_usage_partition_table, /**< An MBR or GPT - the partitions are separate nodes */ +} dmblkid_usage_t; /** - * Create a new dmblkid instance. + * @brief Identify the contents of a node + * + * The geometry comes from DMDRVI_IOCTL_BLOCK_GET_INFO when the node answers + * it (dmdevfs block and partition nodes), otherwise from the file size with + * 512-byte blocks (4096/2048/1024 if a GPT header is found there) - so + * image files can be probed too. A node without a size (not a block device, + * an empty file) is reported as unknown without being read. * - * @return A valid handle on success, or NULL on allocation failure. + * @param node_path Path of the node or image file. + * @param result Set to the result on success; release it with dmblkid_destroy(). + * @return 0 on success - also when nothing was recognized (see + * dmblkid_get_usage()); -EINVAL on bad arguments, -ENOENT if the + * node cannot be opened, -EIO if reading it failed, -ENOMEM. */ -dmod_dmblkid_api(1.0, dmblkid_t, _create, ( void )); +dmod_dmblkid_api(1.0, int, _probe, ( const char* node_path, dmblkid_t** result )); /** - * Destroy an instance created by dmblkid_create(). Safe to call with - * NULL. + * @brief Release a result of dmblkid_probe(). Safe to call with NULL. */ -dmod_dmblkid_api(1.0, void, _destroy, ( dmblkid_t handle )); +dmod_dmblkid_api(1.0, void, _destroy, ( dmblkid_t* result )); + +/** @return What was found, dmblkid_usage_unknown for an invalid result. */ +dmod_dmblkid_api(1.0, dmblkid_usage_t, _get_usage, ( const dmblkid_t* result )); /** - * Example accessor - replace with your module's real API. - * - * @return true if handle is a valid, non-NULL instance. + * @return "vfat", "exfat" or "dmffs" for a filesystem, "mbr" or "gpt" for a + * partition table, NULL if nothing was recognized. + */ +dmod_dmblkid_api(1.0, const char*, _get_type, ( const dmblkid_t* result )); + +/** @return Format version ("FAT12"/"FAT16"/"FAT32", exFAT "1.0", dmffs VERSION), or NULL. */ +dmod_dmblkid_api(1.0, const char*, _get_version, ( const dmblkid_t* result )); + +/** @return Volume label, or NULL if the filesystem has none. */ +dmod_dmblkid_api(1.0, const char*, _get_label, ( const dmblkid_t* result )); + +/** + * @return Filesystem serial ("ABCD-1234" for FAT/exFAT), MBR disk signature + * ("1a2b3c4d") or GPT disk GUID, or NULL if there is none. */ -dmod_dmblkid_api(1.0, bool, _is_valid, ( dmblkid_t handle )); +dmod_dmblkid_api(1.0, const char*, _get_uuid, ( const dmblkid_t* result )); + +/** + * @return Name of the DMOD filesystem module that handles this format, or + * NULL if the ecosystem has none (FAT, exFAT today). + */ +dmod_dmblkid_api(1.0, const char*, _get_module, ( const dmblkid_t* result )); + +/** + * @return true if dmblkid_get_module() can mount the node itself. False for + * partition tables, unknown contents, formats without a module and + * dmffs (it mounts memory-mapped flash, not a node). + */ +dmod_dmblkid_api(1.0, bool, _is_mountable, ( const dmblkid_t* result )); + +/** @return Size of the node in bytes, 0 if unknown. */ +dmod_dmblkid_api(1.0, uint64_t, _get_size, ( const dmblkid_t* result )); + +/** @return Logical block size the partition table was read with, 0 if unknown. */ +dmod_dmblkid_api(1.0, uint32_t, _get_block_size, ( const dmblkid_t* result )); + +/** @return Number of partitions in the table (partition tables only). */ +dmod_dmblkid_api(1.0, uint32_t, _get_partition_count, ( const dmblkid_t* result )); #endif // DMBLKID_H diff --git a/libs/dmpart/CMakeLists.txt b/libs/dmpart/CMakeLists.txt new file mode 100644 index 0000000..0d0bcf7 --- /dev/null +++ b/libs/dmpart/CMakeLists.txt @@ -0,0 +1,23 @@ +# ===================================================================== +# dmpart - partition table parser +# ===================================================================== +# MBR/GPT parser shared by dmblkid (partition table detection) and dmdevfs +# (partition nodes) - see README.md. Built and released with dmblkid. +set(DMOD_MODULE_NAME dmpart) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 1024) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmpart.dmr) + +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + src/dmpart.c +) + +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +add_subdirectory(tests) diff --git a/libs/dmpart/README.md b/libs/dmpart/README.md new file mode 100644 index 0000000..30bcdaf --- /dev/null +++ b/libs/dmpart/README.md @@ -0,0 +1,74 @@ +# dmpart + +Partition table parser - MBR (with extended/logical partitions) and GPT - as +a small DMOD library module. It is a pure parser: it reads the medium through +a function the caller supplies and reports every partition it finds, with no +knowledge of nodes, drivers or files. + +It is the one implementation both users of partition tables share, so they +always agree on what a partition table is: + +- **dmdevfs** creates the partition nodes (`p`) of a block device + from it, +- **dmblkid** reports a medium with a table as `partition_table`, so the whole + disk is never mounted as a filesystem. + +It is built and released together with dmblkid. + +## Usage + +```c +#include "dmpart.h" + +static int read_medium(void* ctx, uint64_t offset, void* buffer, size_t size) +{ + /* read exactly `size` bytes at byte `offset`, 0 on success */ +} + +static bool on_partition(void* ctx, uint32_t number, uint64_t first_lba, uint64_t lba_count) +{ + /* number: MBR 1-4 by slot, logical from 5; GPT entry index + 1 */ + return true; /* false stops the scan */ +} + +dmpart_medium_t medium = { read_medium, my_ctx, block_size, block_count }; +dmpart_info_t info; +if (dmpart_scan(&medium, on_partition, my_ctx, &info) == 0 && info.table != dmpart_table_none) +{ + /* info.table: dmpart_table_mbr / dmpart_table_gpt, info.count partitions, + info.disk_id: MBR disk signature (bytes 0-3) or GPT disk GUID */ +} +``` + +Pass callbacks that are `static` functions of the calling module: the +address of an external function is loaded through `.got`, which x86_64 `ld` +relaxes into an absolute constant the module loader does not relocate. + +## What is accepted + +- **MBR**: `0x55AA` signature, status bytes `0x00`/`0x80`, at least one used + entry, every entry inside the medium. A FAT or exFAT boot sector (a + partitionless "superfloppy") is never taken for an MBR, even if its boot + code looks like partition entries. Extended partitions (types `0x05`, + `0x0F`, `0x85`) are followed through their EBR chain - at most 64 logical + partitions; loops and logical partitions outside the extended partition + are ignored. +- **GPT** (protective MBR, type `0xEE`): the primary header at LBA 1, or the + backup header at the last LBA if the primary header or its entry array + fails its CRC32. Entries outside the usable range are skipped. +- Block sizes 512, 1024, 2048 and 4096; 64-bit offsets throughout. + +A medium that cannot be read is reported as having no table. `dmpart_scan()` +returns `-EINVAL` for a `NULL` info, a `NULL` read function or an +unsupported geometry, `-ENOMEM` if it cannot allocate its block buffer, and +0 otherwise. + +See [include/dmpart.h](include/dmpart.h) for the full declarations. + +## Tests + +`tests/dmpart_test.c` scans an in-memory sparse medium: MBR primaries and +logical partitions, EBR loops and escapes, invalid tables, FAT/exFAT boot +sectors, blank media, GPT with 512/4096-byte blocks, backup header above +4 GiB, damaged entry arrays, both headers damaged, stopping the scan, and +bad arguments. diff --git a/libs/dmpart/dmpart.dmr b/libs/dmpart/dmpart.dmr new file mode 100644 index 0000000..d698e39 --- /dev/null +++ b/libs/dmpart/dmpart.dmr @@ -0,0 +1,24 @@ +# DMOD Resource File for dmpart +# This file specifies where resources should be installed + +# === Core Module === +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Documentation === +# NOTE: ${repo_dir} is the top-level repo root (dmpart is built through +# add_subdirectory() from dmblkid's CMakeLists.txt), so paths are relative to +# the repo root, not to libs/dmpart. +readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/libs/dmpart/README.md] + +# === Header Files === +# Public header + generated _defs.h +inc=./include => ${destination}/${module}/include [origin=${repo_dir}/libs/dmpart/include] [origin=${build_dir}/libs/dmpart/${module}_defs.h] + +# === License === +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +# Adding itself +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/libs/dmpart/${module}.dmr] diff --git a/libs/dmpart/include/dmpart.h b/libs/dmpart/include/dmpart.h new file mode 100644 index 0000000..6f0f454 --- /dev/null +++ b/libs/dmpart/include/dmpart.h @@ -0,0 +1,91 @@ +#ifndef DMPART_H +#define DMPART_H + +#include +#include +#include +#include "dmod_types.h" +#include "dmpart_defs.h" + +/** + * dmpart - partition table parser (MBR incl. extended/logical partitions, GPT). + * + * A pure parser: it reads the medium through a caller-supplied function and + * reports every partition it finds, with no knowledge of nodes, drivers or + * files. dmdevfs uses it to create partition nodes, dmblkid to tell a + * partitioned disk from a filesystem - one implementation, so both always + * agree on what is a partition table. + */ + +/** Kind of partition table found on a medium. */ +typedef enum +{ + dmpart_table_none = 0, /**< No (valid) table - e.g. a superfloppy or a blank medium */ + dmpart_table_mbr, /**< MBR, possibly with an extended partition */ + dmpart_table_gpt, /**< GPT (primary or backup header) */ +} dmpart_table_t; + +/** Size of dmpart_info_t::disk_id. */ +#define DMPART_DISK_ID_SIZE 16u + +/** + * @brief Read exactly @p size bytes at byte @p offset of the medium + * @return 0 on success, a negative value if the read failed or was short + */ +typedef int (*dmpart_read_t)(void* ctx, uint64_t offset, void* buffer, size_t size); + +/** + * @brief Called for every partition found + * + * @param number Partition number: MBR primaries 1-4 by slot, logical + * partitions from 5 in chain order; GPT entry index + 1. + * @param first_lba First block of the partition. + * @param lba_count Number of blocks. + * @return false to stop the scan. + */ +typedef bool (*dmpart_found_t)(void* ctx, uint32_t number, uint64_t first_lba, uint64_t lba_count); + +/** The medium to scan. */ +typedef struct +{ + dmpart_read_t read; /**< Medium access */ + void* ctx; /**< Passed to read */ + uint32_t block_size; /**< Logical block size: 512, 1024, 2048 or 4096 */ + uint64_t block_count; /**< Number of logical blocks (>= 2) */ +} dmpart_medium_t; + +/** What dmpart_scan() found. */ +typedef struct +{ + dmpart_table_t table; + uint32_t count; /**< Partitions reported to the found callback */ + /** + * MBR: the disk signature in bytes 0-3 (little-endian, as on disk), the + * rest zero. GPT: the disk GUID as stored on disk (mixed-endian). + */ + uint8_t disk_id[DMPART_DISK_ID_SIZE]; +} dmpart_info_t; + +/** + * @brief Find the partitions of a medium + * + * - MBR: signature, sane status bytes, >= 1 used entry, every entry inside + * the medium. A FAT or exFAT boot sector (a partitionless "superfloppy") + * is never taken for an MBR, even if its boot code looks like entries. + * Extended partitions are followed through their EBR chain. + * - GPT (protective MBR): primary header at LBA 1, or the backup at the + * last LBA if the primary header or its entry array fails its CRC32. + * + * A medium that cannot be read is reported as having no table. + * + * @param medium The medium. + * @param found Called for every partition, in table order; may be NULL. + * @param found_ctx Passed to @p found. + * @param info Filled with the result (zeroed first). + * @return 0 (info->table tells what was found), -EINVAL for a NULL info, a + * NULL read function or an unsupported geometry, -ENOMEM. + */ +dmod_dmpart_api(1.0, int, _scan, ( const dmpart_medium_t* medium, dmpart_found_t found, + void* found_ctx, dmpart_info_t* info )); + +#endif // DMPART_H diff --git a/libs/dmpart/src/dmpart.c b/libs/dmpart/src/dmpart.c new file mode 100644 index 0000000..bb178a2 --- /dev/null +++ b/libs/dmpart/src/dmpart.c @@ -0,0 +1,424 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmpart.h" +#include +#include + +/* + * MBR: sector 0, signature 0x55 0xAA at 510, four 16-byte entries at 446. + * Types 0x05/0x0F/0x85 are extended partitions: a chain of EBRs, each + * with the logical partition in entry 0 (relative to the EBR) and the + * next EBR in entry 1 (relative to the extended partition start). + * Type 0xEE (protective MBR) means the medium is GPT. + * GPT: header at LBA 1 (backup at the last LBA), entry array CRC32-checked. + * + * Every value read from the medium is validated against the medium size; + * nothing found outside it, overlapping the table or looping is reported. + */ + +#define MBR_SIGNATURE_OFFSET 510u +#define MBR_DISK_SIGNATURE_OFFSET 440u +#define MBR_ENTRIES_OFFSET 446u +#define MBR_ENTRY_SIZE 16u +#define MBR_ENTRY_COUNT 4u +#define MBR_TYPE_GPT_PROTECTIVE 0xEEu +#define MBR_FIRST_LOGICAL_NUMBER 5u +#define MBR_MAX_LOGICAL 64u /* bounds a looping EBR chain */ + +#define GPT_HEADER_MIN_SIZE 92u +#define GPT_DISK_GUID_OFFSET 56u +#define GPT_ENTRY_MIN_SIZE 128u +#define GPT_MAX_ENTRIES 1024u + +#define BLOCK_SIZE_MIN 512u +#define BLOCK_SIZE_MAX 4096u + +typedef struct +{ + const dmpart_medium_t* medium; + dmpart_found_t found; + void* found_ctx; + uint8_t* block; /**< One block buffer */ + dmpart_info_t* info; +} scan_t; + +typedef struct +{ + uint8_t status; + uint8_t type; + uint32_t first_lba; + uint32_t lba_count; +} mbr_entry_t; + +/* ---- little helpers ---- */ + +static uint16_t le16(const uint8_t* p) +{ + return (uint16_t)(p[0] | (p[1] << 8)); +} + +static uint32_t le32(const uint8_t* p) +{ + return (uint32_t)le16(p) | ((uint32_t)le16(p + 2) << 16); +} + +static uint64_t le64(const uint8_t* p) +{ + return (uint64_t)le32(p) | ((uint64_t)le32(p + 4) << 32); +} + +static bool bytes_equal(const uint8_t* a, const char* b, size_t size) +{ + for (size_t i = 0; i < size; i++) + { + if (a[i] != (uint8_t)b[i]) + { + return false; + } + } + return true; +} + +static bool has_boot_signature(const uint8_t* sector) +{ + return sector[MBR_SIGNATURE_OFFSET] == 0x55u && sector[MBR_SIGNATURE_OFFSET + 1u] == 0xAAu; +} + +static bool all_zero(const uint8_t* p, size_t size) +{ + for (size_t i = 0; i < size; i++) + { + if (p[i] != 0) + { + return false; + } + } + return true; +} + +/* CRC32 (IEEE 802.3, reflected) as used by GPT - bitwise, no table to keep flash small. */ +static uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t size) +{ + crc = ~crc; + for (size_t i = 0; i < size; i++) + { + crc ^= data[i]; + for (int bit = 0; bit < 8; bit++) + { + crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); + } + } + return ~crc; +} + +static bool read_block(scan_t* scan, uint64_t lba) +{ + const dmpart_medium_t* m = scan->medium; + return lba < m->block_count && m->read(m->ctx, lba * m->block_size, scan->block, m->block_size) == 0; +} + +static bool in_medium(const scan_t* scan, uint64_t first_lba, uint64_t lba_count) +{ + uint64_t blocks = scan->medium->block_count; + return lba_count != 0 && first_lba != 0 && first_lba < blocks && lba_count <= blocks - first_lba; +} + +static bool report(scan_t* scan, uint32_t number, uint64_t first_lba, uint64_t lba_count) +{ + scan->info->count++; + return scan->found == NULL || scan->found(scan->found_ctx, number, first_lba, lba_count); +} + +/* ---- MBR ---- */ + +static mbr_entry_t mbr_entry(const uint8_t* sector, uint32_t index) +{ + const uint8_t* e = sector + MBR_ENTRIES_OFFSET + index * MBR_ENTRY_SIZE; + mbr_entry_t entry = { e[0], e[4], le32(e + 8), le32(e + 12) }; + return entry; +} + +static bool mbr_entry_used(const mbr_entry_t* entry) +{ + return entry->type != 0 && entry->lba_count != 0; +} + +static bool mbr_is_extended(uint8_t type) +{ + return type == 0x05u || type == 0x0Fu || type == 0x85u; +} + +/* + * FAT and exFAT boot sectors carry the same 0x55AA signature - a + * partitionless "superfloppy", whose boot code may look like entries. + */ +static bool looks_like_boot_sector(const uint8_t* sector) +{ + bool jump = (sector[0] == 0xEBu && sector[2] == 0x90u) || sector[0] == 0xE9u; + return jump && (bytes_equal(sector + 54, "FAT", 3) || bytes_equal(sector + 82, "FAT32", 5) || + bytes_equal(sector + 3, "EXFAT ", 8)); +} + +/* Valid MBR: signature, sane status bytes, >= 1 used entry, all used entries inside the medium. */ +static bool mbr_is_valid(const scan_t* scan, const uint8_t* sector, bool* protective) +{ + bool used = false; + *protective = false; + if (!has_boot_signature(sector) || looks_like_boot_sector(sector)) + { + return false; + } + for (uint32_t i = 0; i < MBR_ENTRY_COUNT; i++) + { + mbr_entry_t entry = mbr_entry(sector, i); + if (entry.status != 0x00u && entry.status != 0x80u) + { + return false; + } + if (!mbr_entry_used(&entry)) + { + continue; + } + used = true; + if (entry.type == MBR_TYPE_GPT_PROTECTIVE) + { + *protective = true; /* may span "the whole disk" as 0xFFFFFFFF - not range checked */ + } + else if (!in_medium(scan, entry.first_lba, entry.lba_count)) + { + return false; + } + } + return used; +} + +/* Walk the EBR chain of one extended partition. Returns false if the caller stopped the scan. */ +static bool mbr_scan_logical(scan_t* scan, const mbr_entry_t* extended, uint32_t* number) +{ + uint64_t ext_start = extended->first_lba; + uint64_t ext_end = ext_start + extended->lba_count; + uint64_t ebr = ext_start; + + for (uint32_t i = 0; i < MBR_MAX_LOGICAL && read_block(scan, ebr) && has_boot_signature(scan->block); i++) + { + mbr_entry_t logical = mbr_entry(scan->block, 0); + mbr_entry_t next = mbr_entry(scan->block, 1); + uint64_t first = ebr + logical.first_lba; + if (mbr_entry_used(&logical) && first + logical.lba_count <= ext_end && + !report(scan, (*number)++, first, logical.lba_count)) + { + return false; + } + uint64_t next_ebr = ext_start + next.first_lba; + if (!mbr_entry_used(&next) || !mbr_is_extended(next.type) || next_ebr <= ebr || next_ebr >= ext_end) + { + break; + } + ebr = next_ebr; + } + return true; +} + +static void mbr_scan(scan_t* scan, const uint8_t* sector_copy) +{ + mbr_entry_t entries[MBR_ENTRY_COUNT]; + for (uint32_t i = 0; i < MBR_ENTRY_COUNT; i++) + { + entries[i] = mbr_entry(sector_copy, i); + } + uint32_t logical_number = MBR_FIRST_LOGICAL_NUMBER; + for (uint32_t i = 0; i < MBR_ENTRY_COUNT; i++) + { + if (!mbr_entry_used(&entries[i])) + { + continue; + } + bool go_on = mbr_is_extended(entries[i].type) + ? mbr_scan_logical(scan, &entries[i], &logical_number) + : report(scan, i + 1u, entries[i].first_lba, entries[i].lba_count); + if (!go_on) + { + return; + } + } +} + +/* ---- GPT ---- */ + +typedef struct +{ + uint64_t first_usable; + uint64_t last_usable; + uint64_t entries_lba; + uint32_t entry_count; + uint32_t entry_size; + uint32_t entries_crc; + uint8_t disk_guid[DMPART_DISK_ID_SIZE]; +} gpt_header_t; + +static bool gpt_read_header(scan_t* scan, uint64_t lba, gpt_header_t* header) +{ + if (!read_block(scan, lba) || !bytes_equal(scan->block, "EFI PART", 8)) + { + return false; + } + uint32_t size = le32(scan->block + 12); + uint32_t crc = le32(scan->block + 16); + if (size < GPT_HEADER_MIN_SIZE || size > scan->medium->block_size || le64(scan->block + 24) != lba) + { + return false; + } + memset(scan->block + 16, 0, 4); /* the CRC is computed with its own field zeroed */ + if (crc32_update(0, scan->block, size) != crc) + { + return false; + } + header->first_usable = le64(scan->block + 40); + header->last_usable = le64(scan->block + 48); + header->entries_lba = le64(scan->block + 72); + header->entry_count = le32(scan->block + 80); + header->entry_size = le32(scan->block + 84); + header->entries_crc = le32(scan->block + 88); + memcpy(header->disk_guid, scan->block + GPT_DISK_GUID_OFFSET, DMPART_DISK_ID_SIZE); + return header->entry_size >= GPT_ENTRY_MIN_SIZE && header->entry_size % 8u == 0 && + scan->medium->block_size % header->entry_size == 0 && header->entry_count <= GPT_MAX_ENTRIES && + header->first_usable <= header->last_usable && header->last_usable < scan->medium->block_count; +} + +static uint64_t gpt_entry_blocks(const scan_t* scan, const gpt_header_t* header) +{ + uint64_t bytes = (uint64_t)header->entry_count * header->entry_size; + return (bytes + scan->medium->block_size - 1u) / scan->medium->block_size; +} + +static bool gpt_entries_valid(scan_t* scan, const gpt_header_t* header) +{ + uint64_t blocks = gpt_entry_blocks(scan, header); + uint64_t remaining = (uint64_t)header->entry_count * header->entry_size; + uint32_t crc = 0; + if (!in_medium(scan, header->entries_lba, blocks == 0 ? 1 : blocks)) + { + return false; + } + for (uint64_t b = 0; b < blocks; b++) + { + if (!read_block(scan, header->entries_lba + b)) + { + return false; + } + size_t chunk = (remaining < scan->medium->block_size) ? (size_t)remaining : scan->medium->block_size; + crc = crc32_update(crc, scan->block, chunk); + remaining -= chunk; + } + return crc == header->entries_crc; +} + +static void gpt_report(scan_t* scan, const gpt_header_t* header) +{ + uint32_t per_block = scan->medium->block_size / header->entry_size; + for (uint32_t i = 0; i < header->entry_count; i++) + { + if (i % per_block == 0 && !read_block(scan, header->entries_lba + i / per_block)) + { + return; + } + const uint8_t* entry = scan->block + (i % per_block) * header->entry_size; + uint64_t first = le64(entry + 32); + uint64_t last = le64(entry + 40); + if (all_zero(entry, 16) || first > last || first < header->first_usable || last > header->last_usable) + { + continue; + } + if (!report(scan, i + 1u, first, last - first + 1u)) + { + return; + } + } +} + +/* Primary header and entries first, the backup at the last LBA if either is damaged. */ +static bool gpt_scan(scan_t* scan) +{ + gpt_header_t header; + bool valid = gpt_read_header(scan, 1, &header) && gpt_entries_valid(scan, &header); + if (!valid) + { + valid = gpt_read_header(scan, scan->medium->block_count - 1u, &header) && gpt_entries_valid(scan, &header); + } + if (valid) + { + memcpy(scan->info->disk_id, header.disk_guid, DMPART_DISK_ID_SIZE); + gpt_report(scan, &header); + } + return valid; +} + +/* ---- entry point ---- */ + +/* 0 with info->table set, or -ENOMEM. */ +static int scan_medium(scan_t* scan) +{ + bool protective = false; + if (!read_block(scan, 0) || !mbr_is_valid(scan, scan->block, &protective)) + { + return 0; + } + if (protective) + { + scan->info->table = gpt_scan(scan) ? dmpart_table_gpt : dmpart_table_none; + return 0; + } + + /* The EBR walk reuses the block buffer - scan from a copy of the MBR. */ + uint8_t* mbr = Dmod_Malloc(scan->medium->block_size); + if (mbr == NULL) + { + return -ENOMEM; + } + memcpy(mbr, scan->block, scan->medium->block_size); + memcpy(scan->info->disk_id, mbr + MBR_DISK_SIGNATURE_OFFSET, 4); + scan->info->table = dmpart_table_mbr; + mbr_scan(scan, mbr); + Dmod_Free(mbr); + return 0; +} + +static bool medium_valid(const dmpart_medium_t* medium) +{ + uint32_t size = medium->block_size; + return medium->read != NULL && size >= BLOCK_SIZE_MIN && size <= BLOCK_SIZE_MAX && + (size & (size - 1u)) == 0 && medium->block_count >= 2u; +} + +dmod_dmpart_api_declaration(1.0, int, _scan, ( const dmpart_medium_t* medium, dmpart_found_t found, + void* found_ctx, dmpart_info_t* info )) +{ + if (info == NULL) + { + return -EINVAL; + } + memset(info, 0, sizeof(*info)); + if (medium == NULL || !medium_valid(medium)) + { + return -EINVAL; + } + scan_t scan = { medium, found, found_ctx, Dmod_Malloc(medium->block_size), info }; + if (scan.block == NULL) + { + return -ENOMEM; + } + int ret = scan_medium(&scan); + Dmod_Free(scan.block); + return ret; +} + +int dmod_init(const Dmod_Config_t *Config) +{ + (void)Config; + return 0; +} + +int dmod_deinit(void) +{ + return 0; +} diff --git a/libs/dmpart/tests/CMakeLists.txt b/libs/dmpart/tests/CMakeLists.txt new file mode 100644 index 0000000..13b2798 --- /dev/null +++ b/libs/dmpart/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +set(DMOD_MODULE_NAME test_dmpart) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 4096) +set(DMOD_PRIORITY 0) + +dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + dmpart_test.c +) + +target_link_libraries(${DMOD_MODULE_NAME} dmpart_if) + +# Same as the dmblkid tests: install the dependencies with dmf-get, run +# through dmod_loader (one with the 64-bit file API - see the README). +find_program(DMF_GET_EXECUTABLE dmf-get HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) +find_program(DMOD_LOADER_EXECUTABLE dmod_loader HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) + +if(DMF_GET_EXECUTABLE AND DMOD_LOADER_EXECUTABLE) + add_test( + NAME ${DMOD_MODULE_NAME} + COMMAND ${CMAKE_COMMAND} -E env DMOD_DMF_DIR=${DMOD_DMF_DIR} + bash -c "${DMF_GET_EXECUTABLE} install -d ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}-local.dmd -y && ${DMOD_LOADER_EXECUTABLE} ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}.dmf" + ) +endif() diff --git a/libs/dmpart/tests/dmpart_test.c b/libs/dmpart/tests/dmpart_test.c new file mode 100644 index 0000000..a3e5205 --- /dev/null +++ b/libs/dmpart/tests/dmpart_test.c @@ -0,0 +1,462 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod_test.h" +#include "dmpart.h" +#include +#include + +/* + * dmpart_scan() on an in-memory medium: a sparse map of written blocks, all + * others read as zeros - so multi-GiB media cost a few blocks of RAM. + */ + +#define MAX_BLOCKS 80u +#define MAX_FOUND 8u + +typedef struct +{ + uint32_t block_size; + uint64_t block_count; + uint64_t lba[MAX_BLOCKS]; + uint8_t* data[MAX_BLOCKS]; + uint32_t used; + bool fail_reads; +} disk_t; + +typedef struct +{ + uint32_t count; + uint32_t stop_after; /* 0: never stop */ + uint32_t number[MAX_FOUND]; + uint64_t first[MAX_FOUND]; + uint64_t blocks[MAX_FOUND]; +} found_t; + +static disk_t g_disk; +static found_t g_found; +static dmpart_info_t g_info; + +void dmod_test_setup(void) +{ + memset(&g_disk, 0, sizeof(g_disk)); + memset(&g_found, 0, sizeof(g_found)); + memset(&g_info, 0xA5, sizeof(g_info)); +} + +void dmod_test_teardown(void) +{ + for (uint32_t i = 0; i < g_disk.used; i++) + { + Dmod_Free(g_disk.data[i]); + } + g_disk.used = 0; +} + +/* ---- the medium ---- */ + +static void disk_init(uint32_t block_size, uint64_t block_count) +{ + g_disk.block_size = block_size; + g_disk.block_count = block_count; +} + +/* The block at @p lba, created (zeroed) on first use. */ +static uint8_t* block(uint64_t lba) +{ + for (uint32_t i = 0; i < g_disk.used; i++) + { + if (g_disk.lba[i] == lba) + { + return g_disk.data[i]; + } + } + uint8_t* data = (g_disk.used < MAX_BLOCKS) ? Dmod_Malloc(g_disk.block_size) : NULL; + if (data != NULL) + { + memset(data, 0, g_disk.block_size); + g_disk.lba[g_disk.used] = lba; + g_disk.data[g_disk.used] = data; + g_disk.used++; + } + return data; +} + +static const uint8_t* find_block(uint64_t lba) +{ + for (uint32_t i = 0; i < g_disk.used; i++) + { + if (g_disk.lba[i] == lba) + { + return g_disk.data[i]; + } + } + return NULL; +} + +/* Static: see dmblkid src/dmblkid_ptable.c - no external function addresses in modules. */ +static int disk_read(void* ctx, uint64_t offset, void* buffer, size_t size) +{ + disk_t* disk = ctx; + uint8_t* out = buffer; + if (disk->fail_reads || offset + size > disk->block_count * disk->block_size) + { + return -EIO; + } + while (size > 0) + { + uint64_t lba = offset / disk->block_size; + size_t in_block = (size_t)(offset % disk->block_size); + size_t chunk = disk->block_size - in_block; + chunk = (chunk < size) ? chunk : size; + const uint8_t* data = find_block(lba); + if (data != NULL) + { + memcpy(out, data + in_block, chunk); + } + else + { + memset(out, 0, chunk); + } + out += chunk; + offset += chunk; + size -= chunk; + } + return 0; +} + +static bool on_found(void* ctx, uint32_t number, uint64_t first_lba, uint64_t lba_count) +{ + found_t* found = ctx; + if (found->count < MAX_FOUND) + { + found->number[found->count] = number; + found->first[found->count] = first_lba; + found->blocks[found->count] = lba_count; + } + found->count++; + return found->stop_after == 0 || found->count < found->stop_after; +} + +static int scan(void) +{ + dmpart_medium_t medium = { disk_read, &g_disk, g_disk.block_size, g_disk.block_count }; + return dmpart_scan(&medium, on_found, &g_found, &g_info); +} + +/* No memcmp in modules. */ +static bool bytes_equal(const uint8_t* a, const uint8_t* b, size_t size) +{ + for (size_t i = 0; i < size; i++) + { + if (a[i] != b[i]) + { + return false; + } + } + return true; +} + +/* ---- builders ---- */ + +static void put16(uint8_t* p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); } +static void put32(uint8_t* p, uint32_t v) { put16(p, (uint16_t)v); put16(p + 2, (uint16_t)(v >> 16)); } +static void put64(uint8_t* p, uint64_t v) { put32(p, (uint32_t)v); put32(p + 4, (uint32_t)(v >> 32)); } + +static void mbr_entry(uint64_t lba, int index, uint8_t status, uint8_t type, uint32_t first, uint32_t count) +{ + uint8_t* s = block(lba); + uint8_t* e = s + 446 + 16 * index; + e[0] = status; + e[4] = type; + put32(e + 8, first); + put32(e + 12, count); + s[510] = 0x55; + s[511] = 0xAA; +} + +static uint32_t crc32(const uint8_t* data, size_t size) +{ + uint32_t crc = 0xFFFFFFFFu; + for (size_t i = 0; i < size; i++) + { + crc ^= data[i]; + for (int bit = 0; bit < 8; bit++) + { + crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); + } + } + return ~crc; +} + +#define GPT_ENTRIES 128u +#define GPT_ENTRY_SIZE 128u + +/* Entry array (16 KiB) starting at @p lba, two partitions; returns its CRC. */ +static uint32_t gpt_entries(uint64_t lba, uint64_t first_usable, uint64_t last_usable) +{ + uint32_t per_block = g_disk.block_size / GPT_ENTRY_SIZE; + uint32_t blocks = GPT_ENTRIES / per_block; + uint8_t* array = Dmod_Malloc(GPT_ENTRIES * GPT_ENTRY_SIZE); + memset(array, 0, GPT_ENTRIES * GPT_ENTRY_SIZE); + memset(array, 0x11, 32); + put64(array + 32, first_usable); + put64(array + 40, first_usable + 99u); + memset(array + 5u * GPT_ENTRY_SIZE, 0x22, 32); /* entry 6: numbers follow the slot */ + put64(array + 5u * GPT_ENTRY_SIZE + 32, first_usable + 100u); + put64(array + 5u * GPT_ENTRY_SIZE + 40, last_usable); + for (uint32_t b = 0; b < blocks; b++) + { + memcpy(block(lba + b), array + b * g_disk.block_size, g_disk.block_size); + } + uint32_t crc = crc32(array, GPT_ENTRIES * GPT_ENTRY_SIZE); + Dmod_Free(array); + return crc; +} + +static void gpt_header(uint64_t lba, uint64_t other, uint64_t entries_lba, uint32_t entries_crc, + uint64_t first_usable, uint64_t last_usable) +{ + uint8_t* h = block(lba); + memcpy(h, "EFI PART", 8); + put32(h + 8, 0x00010000u); + put32(h + 12, 92); + put64(h + 24, lba); + put64(h + 32, other); + put64(h + 40, first_usable); + put64(h + 48, last_usable); + for (int i = 0; i < 16; i++) + { + h[56 + i] = (uint8_t)(0xD0 + i); /* disk GUID */ + } + put64(h + 72, entries_lba); + put32(h + 80, GPT_ENTRIES); + put32(h + 84, GPT_ENTRY_SIZE); + put32(h + 88, entries_crc); + put32(h + 16, crc32(h, 92)); +} + +static void build_gpt(void) +{ + uint64_t last = g_disk.block_count - 1u; + uint64_t array_blocks = GPT_ENTRIES * GPT_ENTRY_SIZE / g_disk.block_size; + uint64_t first_usable = 2u + array_blocks; + uint64_t last_usable = last - array_blocks - 1u; + uint64_t blocks = g_disk.block_count - 1u; + mbr_entry(0, 0, 0x00, 0xEE, 1, (blocks > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t)blocks); + uint32_t crc = gpt_entries(2, first_usable, last_usable); + gpt_entries(last - array_blocks, first_usable, last_usable); + gpt_header(1, last, 2, crc, first_usable, last_usable); + gpt_header(last, 1, last - array_blocks, crc, first_usable, last_usable); +} + +/* ---- MBR ---- */ + +DMOD_TEST_STEP(mbr_primaries_numbered_by_slot) +{ + disk_init(512, 100000); + put32(block(0) + 440, 0x1A2B3C4Du); + mbr_entry(0, 0, 0x80, 0x0C, 2048, 1000); + mbr_entry(0, 2, 0x00, 0x83, 4096, 5000); + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_mbr); + DMOD_TEST_EXPECT_EQ(g_info.count, 2u); + DMOD_TEST_EXPECT_EQ(g_found.count, 2u); + DMOD_TEST_EXPECT_EQ(g_found.number[0], 1u); + DMOD_TEST_EXPECT_EQ(g_found.first[0], 2048u); + DMOD_TEST_EXPECT_EQ(g_found.blocks[0], 1000u); + DMOD_TEST_EXPECT_EQ(g_found.number[1], 3u); + DMOD_TEST_EXPECT_EQ(g_found.first[1], 4096u); + uint8_t id[DMPART_DISK_ID_SIZE] = { 0x4D, 0x3C, 0x2B, 0x1A }; + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_info.disk_id, id, sizeof(id))); +} + +DMOD_TEST_STEP(mbr_logical_partitions_from_5) +{ + disk_init(512, 100000); + mbr_entry(0, 0, 0x00, 0x0C, 2048, 1000); + mbr_entry(0, 1, 0x00, 0x0F, 10000, 50000); /* extended */ + mbr_entry(10000, 0, 0x00, 0x83, 100, 900); /* logical 5 */ + mbr_entry(10000, 1, 0x00, 0x05, 20000, 10000); /* next EBR at 30000 */ + mbr_entry(30000, 0, 0x00, 0x83, 100, 5000); /* logical 6 */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_mbr); + DMOD_TEST_EXPECT_EQ(g_found.count, 3u); + DMOD_TEST_EXPECT_EQ(g_found.number[1], 5u); + DMOD_TEST_EXPECT_EQ(g_found.first[1], 10100u); + DMOD_TEST_EXPECT_EQ(g_found.number[2], 6u); + DMOD_TEST_EXPECT_EQ(g_found.first[2], 30100u); + DMOD_TEST_EXPECT_EQ(g_found.blocks[2], 5000u); +} + +DMOD_TEST_STEP(mbr_ebr_loop_and_escapes_are_ignored) +{ + disk_init(512, 100000); + mbr_entry(0, 0, 0x00, 0x05, 10000, 20000); /* extended 10000-29999 */ + mbr_entry(10000, 0, 0x00, 0x83, 100, 900); /* logical 5 */ + mbr_entry(10000, 1, 0x00, 0x05, 5000, 1000); /* next EBR at 15000 */ + mbr_entry(15000, 0, 0x00, 0x83, 100, 90000); /* runs past the extended partition */ + mbr_entry(15000, 1, 0x00, 0x05, 0, 1000); /* points back to the first EBR */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_mbr); + DMOD_TEST_EXPECT_EQ(g_found.count, 1u); + DMOD_TEST_EXPECT_EQ(g_found.number[0], 5u); +} + +DMOD_TEST_STEP(mbr_invalid_tables_are_none) +{ + disk_init(512, 100000); + mbr_entry(0, 0, 0x7F, 0x0C, 2048, 1000); /* bad status byte */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + + mbr_entry(0, 0, 0x80, 0x0C, 2048, 200000); /* beyond the medium */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + + block(0)[511] = 0; /* no signature */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + DMOD_TEST_EXPECT_EQ(g_found.count, 0u); +} + +DMOD_TEST_STEP(boot_sectors_are_not_mbrs) +{ + disk_init(512, 100000); + uint8_t* s = block(0); + mbr_entry(0, 0, 0x80, 0x06, 63, 2000); /* boot code that looks like an entry */ + s[0] = 0xEB; s[1] = 0x3C; s[2] = 0x90; + memcpy(s + 54, "FAT16 ", 8); + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + + memset(s + 54, 0, 8); + memcpy(s + 82, "FAT32 ", 8); + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + + memset(s + 82, 0, 8); + s[1] = 0x76; + memcpy(s + 3, "EXFAT ", 8); + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + DMOD_TEST_EXPECT_EQ(g_found.count, 0u); +} + +DMOD_TEST_STEP(blank_medium_is_none) +{ + disk_init(512, 100000); + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + DMOD_TEST_EXPECT_EQ(g_info.count, 0u); + uint8_t zero[DMPART_DISK_ID_SIZE] = { 0 }; + DMOD_TEST_EXPECT_TRUE(bytes_equal(g_info.disk_id, zero, sizeof(zero))); +} + +/* ---- GPT ---- */ + +static void expect_gpt(void) +{ + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_gpt); + DMOD_TEST_EXPECT_EQ(g_found.count, 2u); + DMOD_TEST_EXPECT_EQ(g_found.number[0], 1u); + DMOD_TEST_EXPECT_EQ(g_found.blocks[0], 100u); + DMOD_TEST_EXPECT_EQ(g_found.number[1], 6u); + DMOD_TEST_EXPECT_EQ(g_info.disk_id[0], 0xD0); + DMOD_TEST_EXPECT_EQ(g_info.disk_id[15], 0xDF); +} + +DMOD_TEST_STEP(gpt_entries_numbered_by_index) +{ + disk_init(512, 200000); + build_gpt(); + DMOD_TEST_EXPECT_EQ(scan(), 0); + expect_gpt(); + DMOD_TEST_EXPECT_EQ(g_found.first[0], 34u); +} + +DMOD_TEST_STEP(gpt_4k_blocks) +{ + disk_init(4096, 50000); + build_gpt(); + DMOD_TEST_EXPECT_EQ(scan(), 0); + expect_gpt(); + DMOD_TEST_EXPECT_EQ(g_found.first[0], 6u); +} + +DMOD_TEST_STEP(gpt_backup_above_4gib_replaces_damaged_primary) +{ + disk_init(512, 6ull * 1024u * 1024u * 2u); /* 6 GiB */ + build_gpt(); + block(1)[60] ^= 0xFFu; /* primary header CRC mismatch */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + expect_gpt(); +} + +DMOD_TEST_STEP(gpt_backup_replaces_damaged_primary_entries) +{ + disk_init(512, 200000); + build_gpt(); + block(2)[0] ^= 0xFFu; /* primary entry array CRC mismatch */ + DMOD_TEST_EXPECT_EQ(scan(), 0); + expect_gpt(); +} + +DMOD_TEST_STEP(gpt_with_both_headers_damaged_is_none) +{ + disk_init(512, 200000); + build_gpt(); + block(1)[60] ^= 0xFFu; + block(g_disk.block_count - 1u)[60] ^= 0xFFu; + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + DMOD_TEST_EXPECT_EQ(g_found.count, 0u); +} + +/* ---- API contract ---- */ + +DMOD_TEST_STEP(found_can_stop_the_scan) +{ + disk_init(512, 200000); + build_gpt(); + g_found.stop_after = 1; + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_gpt); + DMOD_TEST_EXPECT_EQ(g_found.count, 1u); + DMOD_TEST_EXPECT_EQ(g_info.count, 1u); +} + +DMOD_TEST_STEP(found_may_be_null) +{ + disk_init(512, 100000); + mbr_entry(0, 0, 0x00, 0x0C, 2048, 1000); + dmpart_medium_t medium = { disk_read, &g_disk, 512, 100000 }; + DMOD_TEST_EXPECT_EQ(dmpart_scan(&medium, NULL, NULL, &g_info), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_mbr); + DMOD_TEST_EXPECT_EQ(g_info.count, 1u); +} + +DMOD_TEST_STEP(unreadable_medium_is_none) +{ + disk_init(512, 100000); + mbr_entry(0, 0, 0x00, 0x0C, 2048, 1000); + g_disk.fail_reads = true; + DMOD_TEST_EXPECT_EQ(scan(), 0); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); +} + +DMOD_TEST_STEP(bad_arguments) +{ + dmpart_medium_t medium = { disk_read, &g_disk, 512, 100000 }; + DMOD_TEST_EXPECT_EQ(dmpart_scan(&medium, NULL, NULL, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(dmpart_scan(NULL, NULL, NULL, &g_info), -EINVAL); + DMOD_TEST_EXPECT_EQ(g_info.table, dmpart_table_none); + medium.block_size = 768; + DMOD_TEST_EXPECT_EQ(dmpart_scan(&medium, NULL, NULL, &g_info), -EINVAL); + medium.block_size = 8192; + DMOD_TEST_EXPECT_EQ(dmpart_scan(&medium, NULL, NULL, &g_info), -EINVAL); + medium.block_size = 512; + medium.block_count = 1; + DMOD_TEST_EXPECT_EQ(dmpart_scan(&medium, NULL, NULL, &g_info), -EINVAL); + medium.block_count = 100000; + medium.read = NULL; + DMOD_TEST_EXPECT_EQ(dmpart_scan(&medium, NULL, NULL, &g_info), -EINVAL); +} diff --git a/manifest.dmm b/manifest.dmm index f4b3c58..44e55c6 100644 --- a/manifest.dmm +++ b/manifest.dmm @@ -3,3 +3,11 @@ $include https://github.com/choco-technologies/dmblkid/releases/download/vlatest # Module entry with version placeholder - expanded by $version-available dmblkid https://github.com/choco-technologies/dmblkid/releases/download/v/dmblkid-v-.zip + +# blkid - command line front end (services/blkid), built and released together +# with dmblkid +blkid https://github.com/choco-technologies/dmblkid/releases/download/v/blkid-v-.zip + +# dmpart - partition table parser library (libs/dmpart), used by dmblkid and +# dmdevfs; built and released together with dmblkid +dmpart https://github.com/choco-technologies/dmblkid/releases/download/v/dmpart-v-.zip diff --git a/services/blkid/CMakeLists.txt b/services/blkid/CMakeLists.txt new file mode 100644 index 0000000..36d1e98 --- /dev/null +++ b/services/blkid/CMakeLists.txt @@ -0,0 +1,15 @@ +# ===================================================================== +# blkid - command line front end of dmblkid +# ===================================================================== +# Prints what dmblkid_probe() finds on the nodes given as arguments. +set(DMOD_MODULE_NAME blkid) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 2048) +set(DMOD_PRIORITY 0) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/blkid.dmr) + +dmod_add_executable(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + src/main.c +) + +target_link_libraries(${DMOD_MODULE_NAME} dmblkid_if) diff --git a/services/blkid/README.md b/services/blkid/README.md new file mode 100644 index 0000000..1163fb3 --- /dev/null +++ b/services/blkid/README.md @@ -0,0 +1,31 @@ +# blkid + +Command line front end of [dmblkid](../../README.md): prints what is on each +node given as an argument. + +``` +blkid ... +``` + +One line per node: + +``` +> blkid /dev/dmsdio0/0 /dev/dmsdio0/0p1 /dev/dmsdio0/0p2 +/dev/dmsdio0/0: PTTYPE="mbr" PTUUID="1a2b3c4d" PARTITIONS="2" BLOCK_SIZE="512" SIZE="15931539456" +/dev/dmsdio0/0p1: TYPE="vfat" VERSION="FAT32" LABEL="BOOT" UUID="CAFE-BABE" SIZE="268435456" MOUNTABLE="no" +/dev/dmsdio0/0p2: SIZE="15662055424" +``` + +| Field | Meaning | +|-------|---------| +| `PTTYPE`, `PTUUID`, `PARTITIONS`, `BLOCK_SIZE` | Partition table: `mbr`/`gpt`, disk signature or GUID, number of partitions, block size used | +| `TYPE`, `VERSION`, `LABEL`, `UUID` | Filesystem: `vfat`/`exfat`/`dmffs`, format version, volume label, serial | +| `MODULE`, `MOUNTABLE` | DMOD filesystem module for the type (omitted if there is none), and whether it can mount the node | +| `SIZE` | Node size in bytes | + +A node with nothing recognized (blank, damaged, unsupported) only shows its +`SIZE`. A node that cannot be read shows `cannot probe ()`; the exit +status is then that negative errno. + +Nodes are never listed automatically: reading a character device (a UART, +say) as if it was a disk would block or consume its data. diff --git a/services/blkid/blkid.dmr b/services/blkid/blkid.dmr new file mode 100644 index 0000000..1a2fdd5 --- /dev/null +++ b/services/blkid/blkid.dmr @@ -0,0 +1,20 @@ +# DMOD Resource File for blkid +# This file specifies where resources should be installed + +# === Core Module === +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Documentation === +# NOTE: ${repo_dir} is the top-level repo root (blkid is built through +# add_subdirectory() from dmblkid's CMakeLists.txt), so paths are relative to +# the repo root, not to services/blkid. +readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/services/blkid/README.md] + +# === License === +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +# Adding itself +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/services/blkid/${module}.dmr] diff --git a/services/blkid/src/main.c b/services/blkid/src/main.c new file mode 100644 index 0000000..8beb36a --- /dev/null +++ b/services/blkid/src/main.c @@ -0,0 +1,101 @@ +#include "dmod.h" +#include "dmblkid.h" +#include + +/* + * blkid ... - print what dmblkid finds on each node, one line per node: + * + * /dev/dmsdio0/0: PTTYPE="mbr" PTUUID="1a2b3c4d" PARTITIONS="2" SIZE="15931539456" + * /dev/dmsdio0/0p1: TYPE="vfat" VERSION="FAT32" LABEL="BOOT" UUID="1234-ABCD" SIZE="268435456" MOUNTABLE="no" + * /dev/dmsdio0/0p2: SIZE="15662055424" + * + * Nodes are never listed automatically: reading a character device (a UART) + * as if it was a disk would block or consume its data. + */ + +static void print_usage(const char* prog) +{ + Dmod_Printf("Usage: %s ...\n", prog); + Dmod_Printf("\n"); + Dmod_Printf("Identify the partition table or filesystem on block device nodes\n"); + Dmod_Printf("(e.g. /dev/dmsdio0/0, /dev/dmsdio0/0p1) or image files.\n"); +} + +/* Dmod_Printf does not necessarily support 64-bit conversions. */ +static void print_u64(const char* name, uint64_t value) +{ + char text[21]; + size_t length = sizeof(text) - 1u; + text[length] = '\0'; + do + { + text[--length] = (char)('0' + value % 10u); + value /= 10u; + } while (value != 0); + Dmod_Printf(" %s=\"%s\"", name, text + length); +} + +static void print_string(const char* name, const char* value) +{ + if (value != NULL) + { + Dmod_Printf(" %s=\"%s\"", name, value); + } +} + +static void print_filesystem(const dmblkid_t* result) +{ + print_string("TYPE", dmblkid_get_type(result)); + print_string("VERSION", dmblkid_get_version(result)); + print_string("LABEL", dmblkid_get_label(result)); + print_string("UUID", dmblkid_get_uuid(result)); + print_u64("SIZE", dmblkid_get_size(result)); + print_string("MODULE", dmblkid_get_module(result)); + print_string("MOUNTABLE", dmblkid_is_mountable(result) ? "yes" : "no"); +} + +static void print_partition_table(const dmblkid_t* result) +{ + print_string("PTTYPE", dmblkid_get_type(result)); + print_string("PTUUID", dmblkid_get_uuid(result)); + print_u64("PARTITIONS", dmblkid_get_partition_count(result)); + print_u64("BLOCK_SIZE", dmblkid_get_block_size(result)); + print_u64("SIZE", dmblkid_get_size(result)); +} + +static int probe_node(const char* path) +{ + dmblkid_t* result = NULL; + int ret = dmblkid_probe(path, &result); + if (ret != 0) + { + Dmod_Printf("%s: cannot probe (%d)\n", path, ret); + return ret; + } + Dmod_Printf("%s:", path); + switch (dmblkid_get_usage(result)) + { + case dmblkid_usage_filesystem: print_filesystem(result); break; + case dmblkid_usage_partition_table: print_partition_table(result); break; + default: print_u64("SIZE", dmblkid_get_size(result)); break; + } + Dmod_Printf("\n"); + dmblkid_destroy(result); + return 0; +} + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + print_usage(argv[0]); + return -EINVAL; + } + int status = 0; + for (int i = 1; i < argc; i++) + { + int ret = probe_node(argv[i]); + status = (status == 0) ? ret : status; + } + return status; +} diff --git a/src/dmblkid.c b/src/dmblkid.c index b76e258..35530a1 100644 --- a/src/dmblkid.c +++ b/src/dmblkid.c @@ -1,84 +1,156 @@ #define DMOD_ENABLE_REGISTRATION ON -#include "dmod.h" -#include "dmblkid.h" +#include "dmblkid_internal.h" +#include -/* Example internal state - replace with your module's real fields. */ -struct dmblkid +/* + * Probe order: + * 1. partition table - dmpart, the parser dmdevfs uses for partition + * nodes, so a disk it splits is never reported as one filesystem; it + * does not take FAT/exFAT boot sectors for an MBR, + * 2. FAT and exFAT - strong, fully validated boot sectors, + * 3. dmffs - no magic number, recognized by its TLV chain, so last. + * The first prober that recognizes the contents wins. + */ + +static bool is_valid(const dmblkid_t* result) { - bool valid; -}; + return result != NULL && result->magic == DMBLKID_MAGIC; +} -dmod_dmblkid_api_declaration(1.0, dmblkid_t, _create, ( void )) +static dmblkid_t* create(void) { - /* Dmod_Malloc/Dmod_Free (SAL) are dmod's own heap functions - embedded - * targets don't necessarily link a libc allocator, so use these instead - * of malloc()/free() in module code. */ - struct dmblkid *instance = Dmod_Malloc(sizeof(*instance)); - if (instance == NULL) + dmblkid_t* result = Dmod_Malloc(sizeof(*result)); + if (result != NULL) { - return NULL; + memset(result, 0, sizeof(*result)); + result->magic = DMBLKID_MAGIC; + result->usage = dmblkid_usage_unknown; } - - instance->valid = true; - return instance; + return result; } -dmod_dmblkid_api_declaration(1.0, void, _destroy, ( dmblkid_t handle )) +static int run_probers(const dmblkid_source_t* source, dmblkid_t* result) { - Dmod_Free(handle); + int ret = dmblkid_probe_ptable(source, result); + if (ret == 0) + { + ret = dmblkid_probe_fat(source, result); + } + if (ret == 0) + { + ret = dmblkid_probe_exfat(source, result); + } + if (ret == 0) + { + ret = dmblkid_probe_dmffs(source, result); + } + return ret; } -dmod_dmblkid_api_declaration(1.0, bool, _is_valid, ( dmblkid_t handle )) +dmod_dmblkid_api_declaration(1.0, int, _probe, ( const char* node_path, dmblkid_t** result )) { - return handle != NULL && handle->valid; + if (node_path == NULL || result == NULL) + { + return -EINVAL; + } + *result = NULL; + dmblkid_source_t source = { 0 }; + int ret = dmblkid_source_open(&source, node_path); + if (ret != 0) + { + return ret; + } + /* + * No size: not a block device (a UART would block the first read) or an + * empty file - nothing to identify, nothing is read. + */ + dmblkid_t* probe = create(); + ret = (probe != NULL) ? 0 : -ENOMEM; + if (ret == 0 && source.size != 0) + { + ret = run_probers(&source, probe); + } + dmblkid_source_close(&source); + if (ret < 0) + { + dmblkid_destroy(probe); + return ret; + } + probe->size = source.size; + *result = probe; + return 0; } -/** - * @brief Pre-initialization function for the module. - * - * @note This function is optional. You can remove it if you don't need it. - * - * This function is called when the module enabling is in progress. - * - * You can use this function to load the required dependencies, such as - * other modules. Please be aware that the module is not fully initialized, - * so not all the API functions are available - you can check if the API - * is connected by calling the Dmod_IsFunctionConnected() function. - */ -void dmod_preinit(void) +dmod_dmblkid_api_declaration(1.0, void, _destroy, ( dmblkid_t* result )) { - if(Dmod_IsFunctionConnected( Dmod_Printf )) + if (!is_valid(result)) { - Dmod_Printf("API is connected!\n"); + return; } + Dmod_Free(result->version); + Dmod_Free(result->label); + Dmod_Free(result->uuid); + result->magic = 0; + Dmod_Free(result); +} + +dmod_dmblkid_api_declaration(1.0, dmblkid_usage_t, _get_usage, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->usage : dmblkid_usage_unknown; +} + +dmod_dmblkid_api_declaration(1.0, const char*, _get_type, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->type : NULL; +} + +dmod_dmblkid_api_declaration(1.0, const char*, _get_version, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->version : NULL; +} + +dmod_dmblkid_api_declaration(1.0, const char*, _get_label, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->label : NULL; +} + +dmod_dmblkid_api_declaration(1.0, const char*, _get_uuid, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->uuid : NULL; +} + +dmod_dmblkid_api_declaration(1.0, const char*, _get_module, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->module : NULL; +} + +dmod_dmblkid_api_declaration(1.0, bool, _is_mountable, ( const dmblkid_t* result )) +{ + return is_valid(result) && result->mountable; +} + +dmod_dmblkid_api_declaration(1.0, uint64_t, _get_size, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->size : 0; +} + +dmod_dmblkid_api_declaration(1.0, uint32_t, _get_block_size, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->block_size : 0; +} + +dmod_dmblkid_api_declaration(1.0, uint32_t, _get_partition_count, ( const dmblkid_t* result )) +{ + return is_valid(result) ? result->partition_count : 0; } -/** - * @brief Initialization function for the module. - * - * This function is called when the module is enabled. - * Please use this function to initialize the module, for instance: - * - initialize the module variables - * - initialize the module hardware - * - allocate memory - */ int dmod_init(const Dmod_Config_t *Config) { - Dmod_Printf("Hello, World!\n"); + (void)Config; return 0; } -/** - * @brief De-initialization function for the module. - * - * This function is called when the module is disabled. - * Please use this function to de-initialize the module, for instance: - * - free memory - * - de-initialize the module hardware - * - de-initialize the module variables - */ int dmod_deinit(void) { - Dmod_Printf("Goodbye, World!\n"); return 0; } diff --git a/src/dmblkid_dmffs.c b/src/dmblkid_dmffs.c new file mode 100644 index 0000000..ff3b416 --- /dev/null +++ b/src/dmblkid_dmffs.c @@ -0,0 +1,112 @@ +#include "dmblkid_internal.h" + +/* + * dmffs (choco-technologies/dmffs): a chain of little-endian TLVs + * { uint32 type; uint32 length; value[length] }. make_dmffs writes + * VERSION("1.0") { FILE | DIR }* END(0xFFFFFFFF) + * at the top level (DIR/FILE nest their contents in their value). + * + * There is no magic number, so the chain itself is the signature: an + * optional VERSION with a short printable value, then top-level FILE/DIR + * entries that all fit in the node, up to END. Without a VERSION the chain + * must reach END within DMFFS_MAX_TOP_LEVEL entries. + * + * The dmffs module mounts memory-mapped flash (flash_addr/flash_size), not a + * node, so an image found on a node is reported but not mountable. + */ + +#define DMFFS_TLV_FILE 1u +#define DMFFS_TLV_DIR 2u +#define DMFFS_TLV_VERSION 3u +#define DMFFS_TLV_END 0xFFFFFFFFu +#define DMFFS_TLV_HEADER_SIZE 8u +#define DMFFS_VERSION_MAX 16u +#define DMFFS_MAX_TOP_LEVEL 256u + +static int read_tlv(const dmblkid_source_t* source, uint64_t offset, uint32_t* type, uint32_t* length) +{ + uint8_t header[DMFFS_TLV_HEADER_SIZE]; + int ret = dmblkid_source_read(source, offset, header, sizeof(header)); + *type = dmblkid_le32(header); + *length = dmblkid_le32(header + 4); + return ret; +} + +/* The VERSION value if it is short printable ASCII; 1 if it is, 0 if not, < 0 on error. */ +static int read_version(const dmblkid_source_t* source, uint32_t length, char* version) +{ + if (length == 0 || length > DMFFS_VERSION_MAX) + { + return 0; + } + int ret = dmblkid_source_read(source, DMFFS_TLV_HEADER_SIZE, version, length); + if (ret != 0) + { + return dmblkid_not_found_if_outside(ret); + } + for (uint32_t i = 0; i < length; i++) + { + if (version[i] < 0x20 || version[i] > 0x7E) + { + return 0; + } + } + version[length] = '\0'; + return 1; +} + +/* 1 if the top-level chain from @p offset is well formed, 0 if not, < 0 on error. */ +static int walk_top_level(const dmblkid_source_t* source, uint64_t offset, bool has_version) +{ + uint32_t entries = 0; + for (; entries < DMFFS_MAX_TOP_LEVEL; entries++) + { + uint32_t type, length; + int ret = read_tlv(source, offset, &type, &length); + if (ret != 0) + { + return dmblkid_not_found_if_outside(ret); + } + if (type == DMFFS_TLV_END) + { + return (has_version || entries != 0) ? 1 : 0; + } + uint64_t next = offset + DMFFS_TLV_HEADER_SIZE + length; + if ((type != DMFFS_TLV_FILE && type != DMFFS_TLV_DIR) || (source->size != 0 && next > source->size)) + { + return 0; + } + offset = next; + } + return has_version ? 1 : 0; /* a long root without END: trust it only with a VERSION */ +} + +static int describe(dmblkid_t* result, const char* version) +{ + dmblkid_set_filesystem(result, "dmffs", "dmffs", false); + int ret = (version[0] != '\0') ? dmblkid_set_string(&result->version, version) : 0; + return (ret < 0) ? ret : 1; +} + +int dmblkid_probe_dmffs(const dmblkid_source_t* source, dmblkid_t* result) +{ + char version[DMFFS_VERSION_MAX + 1u] = { 0 }; + uint32_t type, length; + int ret = read_tlv(source, 0, &type, &length); + if (ret != 0) + { + return dmblkid_not_found_if_outside(ret); + } + uint64_t offset = 0; + if (type == DMFFS_TLV_VERSION) + { + ret = read_version(source, length, version); + offset = DMFFS_TLV_HEADER_SIZE + (uint64_t)length; + } + else if (type == DMFFS_TLV_FILE || type == DMFFS_TLV_DIR) + { + ret = 1; + } + ret = (ret == 1) ? walk_top_level(source, offset, version[0] != '\0') : ret; + return (ret == 1) ? describe(result, version) : ret; +} diff --git a/src/dmblkid_exfat.c b/src/dmblkid_exfat.c new file mode 100644 index 0000000..b31a593 --- /dev/null +++ b/src/dmblkid_exfat.c @@ -0,0 +1,200 @@ +#include "dmblkid_internal.h" + +/* + * exFAT (Microsoft exFAT specification). Recognized only if: + * - the boot sector has the "EXFAT " name, the zeroed legacy BPB area + * and 0x55AA, + * - sector/cluster shifts, FAT count and every region (FAT, cluster heap, + * root directory) are consistent with the volume length, and the volume + * fits in the node, + * - the boot region checksum (sector 11) matches sectors 0-10, so a + * damaged boot region is not reported as a filesystem. + * The label is the volume label entry in the first root directory cluster. + */ + +#define EXFAT_MIN_VOLUME_BYTES (1024u * 1024u) +#define EXFAT_CHECKSUM_SECTOR 11u +#define EXFAT_DIR_ENTRY_SIZE 32u +#define EXFAT_MAX_ROOT_ENTRIES 1024u /* bounds the label search */ +#define EXFAT_ENTRY_END 0x00u +#define EXFAT_ENTRY_LABEL 0x83u +#define EXFAT_LABEL_MAX_CHARS 11u + +typedef struct +{ + uint64_t volume_length; /* sectors */ + uint32_t fat_offset; + uint32_t fat_length; + uint32_t heap_offset; + uint32_t cluster_count; + uint32_t root_cluster; + uint32_t serial; + uint8_t revision_major; + uint8_t revision_minor; + uint8_t sector_shift; + uint8_t cluster_shift; /* sectors per cluster, log2 */ + uint8_t fat_count; +} exfat_boot_t; + +static bool exfat_header_ok(const uint8_t* s) +{ + for (uint32_t i = 11; i < 64; i++) + { + if (s[i] != 0) + { + return false; /* MustBeZero: where FAT keeps its BPB */ + } + } + return s[0] == 0xEBu && s[1] == 0x76u && s[2] == 0x90u && dmblkid_bytes_equal(s + 3, "EXFAT ", 8) && + dmblkid_has_boot_signature(s); +} + +static void exfat_read_boot(const uint8_t* s, exfat_boot_t* b) +{ + b->volume_length = dmblkid_le64(s + 72); + b->fat_offset = dmblkid_le32(s + 80); + b->fat_length = dmblkid_le32(s + 84); + b->heap_offset = dmblkid_le32(s + 88); + b->cluster_count = dmblkid_le32(s + 92); + b->root_cluster = dmblkid_le32(s + 96); + b->serial = dmblkid_le32(s + 100); + b->revision_minor = s[104]; + b->revision_major = s[105]; + b->sector_shift = s[108]; + b->cluster_shift = s[109]; + b->fat_count = s[110]; +} + +static bool exfat_layout_ok(const exfat_boot_t* b, uint64_t node_size) +{ + if (b->sector_shift < 9u || b->sector_shift > 12u || b->cluster_shift > 25u - b->sector_shift || + b->fat_count < 1u || b->fat_count > 2u) + { + return false; + } + uint64_t fat_end = (uint64_t)b->fat_offset + (uint64_t)b->fat_length * b->fat_count; + uint64_t min_length = EXFAT_MIN_VOLUME_BYTES >> b->sector_shift; + bool regions_ok = b->volume_length >= min_length && b->fat_offset >= 24u && b->fat_length != 0 && + fat_end <= b->heap_offset && b->heap_offset < b->volume_length && + b->cluster_count != 0 && + b->cluster_count <= ((b->volume_length - b->heap_offset) >> b->cluster_shift) && + b->root_cluster >= 2u && b->root_cluster <= (uint64_t)b->cluster_count + 1u; + bool fits_ok = node_size == 0 || b->volume_length <= (node_size >> b->sector_shift); + return regions_ok && fits_ok; +} + +static uint32_t exfat_checksum(uint32_t checksum, const uint8_t* data, uint32_t size, bool boot_sector) +{ + for (uint32_t i = 0; i < size; i++) + { + if (boot_sector && (i == 106u || i == 107u || i == 112u)) + { + continue; /* VolumeFlags and PercentInUse change at run time */ + } + checksum = ((checksum & 1u) ? 0x80000000u : 0u) + (checksum >> 1) + data[i]; + } + return checksum; +} + +/* 1 if sector 11 repeats the checksum of sectors 0-10, 0 if not, < 0 on error. */ +static int exfat_boot_checksum_ok(const dmblkid_source_t* source, const exfat_boot_t* b) +{ + uint32_t size = 1u << b->sector_shift; + uint8_t* sector = Dmod_Malloc(size); + if (sector == NULL) + { + return -ENOMEM; + } + uint32_t checksum = 0; + int ret = 0; + for (uint32_t i = 0; i < EXFAT_CHECKSUM_SECTOR && ret == 0; i++) + { + ret = dmblkid_source_read(source, (uint64_t)i * size, sector, size); + checksum = (ret == 0) ? exfat_checksum(checksum, sector, size, i == 0) : checksum; + } + ret = (ret == 0) ? dmblkid_source_read(source, (uint64_t)EXFAT_CHECKSUM_SECTOR * size, sector, size) : ret; + bool match = ret == 0; + for (uint32_t i = 0; match && i < size; i += 4u) + { + match = dmblkid_le32(sector + i) == checksum; + } + Dmod_Free(sector); + return (ret < 0) ? dmblkid_not_found_if_outside(ret) : (match ? 1 : 0); +} + +/* The volume label entry of the first root directory cluster, if any. */ +static int exfat_find_label(const dmblkid_source_t* source, const exfat_boot_t* b, char** label) +{ + uint64_t sector = b->heap_offset + ((uint64_t)(b->root_cluster - 2u) << b->cluster_shift); + uint64_t offset = sector << b->sector_shift; + uint64_t entries = ((uint64_t)1u << (b->cluster_shift + b->sector_shift)) / EXFAT_DIR_ENTRY_SIZE; + uint8_t entry[EXFAT_DIR_ENTRY_SIZE]; + for (uint64_t i = 0; i < entries && i < EXFAT_MAX_ROOT_ENTRIES; i++, offset += EXFAT_DIR_ENTRY_SIZE) + { + int ret = dmblkid_source_read(source, offset, entry, sizeof(entry)); + if (ret != 0 || entry[0] == EXFAT_ENTRY_END) + { + return dmblkid_not_found_if_outside(ret); + } + if (entry[0] == EXFAT_ENTRY_LABEL) + { + uint8_t chars = (entry[1] <= EXFAT_LABEL_MAX_CHARS) ? entry[1] : EXFAT_LABEL_MAX_CHARS; + return dmblkid_set_label16(label, entry + 2, chars); + } + } + return 0; +} + +/* "." of the FileSystemRevision field. */ +static int exfat_set_version(dmblkid_t* result, const exfat_boot_t* b) +{ + char text[8]; + size_t length = 0; + uint8_t parts[2] = { b->revision_major, b->revision_minor }; + for (int p = 0; p < 2; p++) + { + uint8_t value = parts[p]; + if (value >= 100u) + { + text[length++] = (char)('0' + value / 100u); + } + if (value >= 10u) + { + text[length++] = (char)('0' + (value / 10u) % 10u); + } + text[length++] = (char)('0' + value % 10u); + text[length++] = (p == 0) ? '.' : '\0'; + } + return dmblkid_set_string(&result->version, text); +} + +static int exfat_describe(const dmblkid_source_t* source, const exfat_boot_t* b, dmblkid_t* result) +{ + /* No DMOD filesystem module mounts exFAT yet. */ + dmblkid_set_filesystem(result, "exfat", NULL, false); + int ret = exfat_set_version(result, b); + ret = (ret < 0) ? ret : dmblkid_set_serial(&result->uuid, b->serial); + ret = (ret < 0) ? ret : exfat_find_label(source, b, &result->label); + return (ret < 0) ? ret : 1; +} + +int dmblkid_probe_exfat(const dmblkid_source_t* source, dmblkid_t* result) +{ + uint8_t* sector = Dmod_Malloc(DMBLKID_SECTOR_SIZE); + if (sector == NULL) + { + return -ENOMEM; + } + exfat_boot_t boot = { 0 }; + bool candidate = false; + int ret = dmblkid_source_read(source, 0, sector, DMBLKID_SECTOR_SIZE); + if (ret == 0 && exfat_header_ok(sector)) + { + exfat_read_boot(sector, &boot); + candidate = exfat_layout_ok(&boot, source->size); + } + Dmod_Free(sector); + ret = candidate ? exfat_boot_checksum_ok(source, &boot) : ret; + ret = (ret == 1) ? exfat_describe(source, &boot, result) : ret; + return dmblkid_not_found_if_outside(ret); +} diff --git a/src/dmblkid_fat.c b/src/dmblkid_fat.c new file mode 100644 index 0000000..b8fe6cb --- /dev/null +++ b/src/dmblkid_fat.c @@ -0,0 +1,197 @@ +#include "dmblkid_internal.h" + +/* + * FAT12/16/32 (Microsoft FAT specification). A boot sector is taken for FAT + * only if the whole BPB is consistent - not just the 0x55AA marker: + * - x86 jump, 0x55AA at 510, + * - bytes/sector 512..4096 and sectors/cluster 1..128 (powers of 2), + * - >= 1 reserved sector, 1..4 FATs, a valid media byte, + * - FATs, root directory and data region inside the volume, >= 1 cluster, + * - FATs large enough for every cluster, volume inside the node. + * FAT32 is the BPB with a zero 16-bit FAT size; otherwise the cluster count + * decides between FAT12 (< 4085) and FAT16 (< 65525). + * + * The label is the volume label entry of the root directory (what the OS + * shows), else the one in the extended BPB; "NO NAME" means none. + */ + +#define FAT12_MAX_CLUSTERS 4084u +#define FAT16_MAX_CLUSTERS 65524u +#define FAT_DIR_ENTRY_SIZE 32u +#define FAT_MAX_ROOT_ENTRIES 1024u /* bounds the label search */ +#define FAT_ATTR_VOLUME_ID 0x08u +#define FAT_ATTR_DIRECTORY 0x10u +#define FAT_ATTR_LONG_NAME 0x0Fu +#define FAT_EXT_SIGNATURE 0x29u +#define FAT_EXT_SIGNATURE_OLD 0x28u /* serial only, no label */ +#define FAT_LABEL_SIZE 11u + +typedef struct +{ + uint32_t bytes_per_sector; + uint32_t sectors_per_cluster; + uint32_t reserved; + uint32_t fat_count; + uint32_t root_entries; + uint32_t total_sectors; + uint32_t fat_size; + uint32_t root_cluster; /* FAT32 only */ + uint32_t root_dir_sectors; /* FAT12/16 only */ + uint32_t first_data_sector; + uint32_t clusters; + unsigned bits; /* 12, 16 or 32 */ +} fat_geometry_t; + +static bool fat_boot_code_ok(const uint8_t* s) +{ + bool jump = (s[0] == 0xEBu && s[2] == 0x90u) || s[0] == 0xE9u; + bool media = s[21] == 0xF0u || s[21] >= 0xF8u; + return jump && media && dmblkid_has_boot_signature(s); +} + +/* The raw BPB fields; false if one of them is out of range on its own. */ +static bool fat_read_bpb(const uint8_t* s, fat_geometry_t* g) +{ + uint16_t total16 = dmblkid_le16(s + 19); + uint16_t fat16 = dmblkid_le16(s + 22); + g->bytes_per_sector = dmblkid_le16(s + 11); + g->sectors_per_cluster = s[13]; + g->reserved = dmblkid_le16(s + 14); + g->fat_count = s[16]; + g->root_entries = dmblkid_le16(s + 17); + g->total_sectors = (total16 != 0) ? total16 : dmblkid_le32(s + 32); + g->fat_size = (fat16 != 0) ? fat16 : dmblkid_le32(s + 36); + g->root_cluster = (fat16 != 0) ? 0 : dmblkid_le32(s + 44); + g->bits = (fat16 != 0) ? 16u : 32u; /* 12 vs 16 decided by the cluster count */ + + bool fat32_ok = fat16 != 0 || (g->root_entries == 0 && total16 == 0 && g->root_cluster >= 2u); + return g->bytes_per_sector >= DMBLKID_SECTOR_SIZE && g->bytes_per_sector <= DMBLKID_MAX_BLOCK_SIZE && + dmblkid_is_power_of_2(g->bytes_per_sector) && dmblkid_is_power_of_2(g->sectors_per_cluster) && + g->reserved != 0 && g->fat_count != 0 && g->fat_count <= 4u && g->total_sectors != 0 && + g->fat_size != 0 && fat32_ok && (fat16 == 0 || g->root_entries != 0); +} + +/* Regions and cluster count; false if they do not add up. */ +static bool fat_layout_ok(fat_geometry_t* g, uint64_t node_size) +{ + uint64_t root_bytes = (uint64_t)g->root_entries * FAT_DIR_ENTRY_SIZE; + g->root_dir_sectors = (uint32_t)((root_bytes + g->bytes_per_sector - 1u) / g->bytes_per_sector); + uint64_t first_data = (uint64_t)g->reserved + (uint64_t)g->fat_count * g->fat_size + g->root_dir_sectors; + if (first_data >= g->total_sectors) + { + return false; + } + g->first_data_sector = (uint32_t)first_data; + g->clusters = (g->total_sectors - g->first_data_sector) / g->sectors_per_cluster; + if (g->bits == 16u) + { + g->bits = (g->clusters <= FAT12_MAX_CLUSTERS) ? 12u : 16u; + } + bool clusters_ok = g->clusters != 0 && (g->bits == 32u || g->clusters <= FAT16_MAX_CLUSTERS) && + (g->bits != 32u || g->root_cluster < (uint64_t)g->clusters + 2u); + bool fat_ok = (uint64_t)g->fat_size * g->bytes_per_sector * 8u >= ((uint64_t)g->clusters + 2u) * g->bits; + bool fits_ok = node_size == 0 || (uint64_t)g->total_sectors * g->bytes_per_sector <= node_size; + return clusters_ok && fat_ok && fits_ok; +} + +/* Where the label search starts and how many entries it may look at. */ +static void fat_root_dir(const fat_geometry_t* g, uint64_t* offset, uint32_t* entries) +{ + uint64_t sector; + if (g->bits == 32u) + { + sector = g->first_data_sector + (uint64_t)(g->root_cluster - 2u) * g->sectors_per_cluster; + *entries = g->sectors_per_cluster * g->bytes_per_sector / FAT_DIR_ENTRY_SIZE; /* first cluster */ + } + else + { + sector = (uint64_t)g->reserved + (uint64_t)g->fat_count * g->fat_size; + *entries = g->root_entries; + } + *offset = sector * g->bytes_per_sector; + if (*entries > FAT_MAX_ROOT_ENTRIES) + { + *entries = FAT_MAX_ROOT_ENTRIES; + } +} + +/* 1 and the label in *label if a volume label entry exists, 0 if not, < 0 on error. */ +static int fat_find_root_label(const dmblkid_source_t* source, const fat_geometry_t* g, char** label) +{ + uint8_t entry[FAT_DIR_ENTRY_SIZE]; + uint64_t offset; + uint32_t entries; + fat_root_dir(g, &offset, &entries); + for (uint32_t i = 0; i < entries; i++, offset += FAT_DIR_ENTRY_SIZE) + { + int ret = dmblkid_source_read(source, offset, entry, sizeof(entry)); + if (ret != 0 || entry[0] == 0x00u) + { + return dmblkid_not_found_if_outside(ret); + } + uint8_t attr = entry[11]; + bool volume = (attr & (FAT_ATTR_VOLUME_ID | FAT_ATTR_DIRECTORY)) == FAT_ATTR_VOLUME_ID; + if (entry[0] == 0xE5u || attr == FAT_ATTR_LONG_NAME || !volume) + { + continue; + } + if (entry[0] == 0x05u) + { + entry[0] = 0xE5u; /* KANJI lead byte escape */ + } + ret = dmblkid_set_label8(label, entry, FAT_LABEL_SIZE); + return (ret < 0) ? ret : 1; + } + return 0; +} + +/* Serial and (fallback) label from the extended BPB. */ +static int fat_read_ext_bpb(const uint8_t* s, const fat_geometry_t* g, dmblkid_t* result, bool want_label) +{ + const uint8_t* ext = s + ((g->bits == 32u) ? 64u : 36u); /* drive number */ + if (ext[2] != FAT_EXT_SIGNATURE && ext[2] != FAT_EXT_SIGNATURE_OLD) + { + return 0; + } + int ret = dmblkid_set_serial(&result->uuid, dmblkid_le32(ext + 3)); + if (ret == 0 && want_label && ext[2] == FAT_EXT_SIGNATURE && + !dmblkid_bytes_equal(ext + 7, "NO NAME ", FAT_LABEL_SIZE)) + { + ret = dmblkid_set_label8(&result->label, ext + 7, FAT_LABEL_SIZE); + } + return ret; +} + +static int fat_describe(const dmblkid_source_t* source, const uint8_t* s, const fat_geometry_t* g, + dmblkid_t* result) +{ + /* No DMOD filesystem module mounts FAT yet. */ + dmblkid_set_filesystem(result, "vfat", NULL, false); + const char* version = (g->bits == 12u) ? "FAT12" : (g->bits == 16u) ? "FAT16" : "FAT32"; + int ret = dmblkid_set_string(&result->version, version); + int found = (ret < 0) ? ret : fat_find_root_label(source, g, &result->label); + if (found < 0) + { + return found; + } + ret = fat_read_ext_bpb(s, g, result, found == 0); + return (ret < 0) ? ret : 1; +} + +int dmblkid_probe_fat(const dmblkid_source_t* source, dmblkid_t* result) +{ + uint8_t* sector = Dmod_Malloc(DMBLKID_SECTOR_SIZE); + if (sector == NULL) + { + return -ENOMEM; + } + fat_geometry_t geometry; + int ret = dmblkid_source_read(source, 0, sector, DMBLKID_SECTOR_SIZE); + if (ret == 0 && fat_boot_code_ok(sector) && fat_read_bpb(sector, &geometry) && + fat_layout_ok(&geometry, source->size)) + { + ret = fat_describe(source, sector, &geometry, result); + } + Dmod_Free(sector); + return dmblkid_not_found_if_outside(ret); +} diff --git a/src/dmblkid_internal.h b/src/dmblkid_internal.h new file mode 100644 index 0000000..6151d7f --- /dev/null +++ b/src/dmblkid_internal.h @@ -0,0 +1,133 @@ +/** + * @file dmblkid_internal.h + * @brief Shared state and helpers of the dmblkid probers + */ +#ifndef DMBLKID_INTERNAL_H +#define DMBLKID_INTERNAL_H + +#include "dmod.h" +#include "dmblkid.h" +#include + +#define DMBLKID_MAGIC 0x424C4B49u /* 'BLKI' */ +#define DMBLKID_SECTOR_SIZE 512u +#define DMBLKID_MAX_BLOCK_SIZE 4096u + +/* + * Probe result. type/module point to string literals chosen in code (never + * freed); version/label/uuid are read from the medium and heap-owned. + * + * No prober keeps a table of pointers in initialized data: the module loader + * relocates only .got, so such a table would keep link-time addresses on + * target. Probers are called one after another in dmblkid.c instead. + */ +struct dmblkid +{ + uint32_t magic; + dmblkid_usage_t usage; + const char* type; + const char* module; + bool mountable; + char* version; + char* label; + char* uuid; + uint64_t size; + uint32_t block_size; + uint32_t partition_count; +}; + +/* The node being probed. All reads are bounds-checked against size when it is known. */ +typedef struct +{ + void* file; + uint64_t size; /**< Bytes, 0 if unknown */ + uint32_t block_size; /**< From DMDRVI_IOCTL_BLOCK_GET_INFO, 0 if the node does not answer it */ +} dmblkid_source_t; + +/* + * Every prober returns 1 if it recognized the contents (and filled the + * result), 0 if not, or a negative errno (I/O error, no memory) that aborts + * the probe. + */ +int dmblkid_probe_ptable(const dmblkid_source_t* source, dmblkid_t* result); +int dmblkid_probe_fat(const dmblkid_source_t* source, dmblkid_t* result); +int dmblkid_probe_exfat(const dmblkid_source_t* source, dmblkid_t* result); +int dmblkid_probe_dmffs(const dmblkid_source_t* source, dmblkid_t* result); + +/* ---- source access (dmblkid_source.c) ---- */ + +int dmblkid_source_open(dmblkid_source_t* source, const char* path); +void dmblkid_source_close(dmblkid_source_t* source); + +/** Read exactly @p size bytes at @p offset: 0, -EIO (failed/short) or -ERANGE (past the end). */ +int dmblkid_source_read(const dmblkid_source_t* source, uint64_t offset, void* buffer, size_t size); + +/** A structure that does not fit in the node is simply not there: -ERANGE becomes 0 ("not recognized"). */ +static inline int dmblkid_not_found_if_outside(int ret) +{ + return (ret == -ERANGE) ? 0 : ret; +} + +/* ---- result helpers (dmblkid_util.c) ---- */ + +/** Fill the result for a recognized filesystem. module may be NULL. */ +void dmblkid_set_filesystem(dmblkid_t* result, const char* type, const char* module, bool mountable); + +/** Take a heap copy of @p value into @p field (freeing the previous one). -ENOMEM on failure. */ +int dmblkid_set_string(char** field, const char* value); + +/** "ABCD-1234" from a 32-bit FAT/exFAT volume serial. */ +int dmblkid_set_serial(char** field, uint32_t serial); + +/** + * Label from a fixed-size, space-padded 8-bit field: trailing spaces and NULs + * are dropped, non-ASCII bytes become '?'. An empty label leaves the field NULL. + */ +int dmblkid_set_label8(char** field, const uint8_t* label, size_t size); + +/** Label from UTF-16LE code units (UTF-8 result, surrogates become '?'). */ +int dmblkid_set_label16(char** field, const uint8_t* label, size_t units); + +/** Lowercase or uppercase hex of @p digits digits of @p value into @p out (not terminated). */ +void dmblkid_hex(char* out, uint64_t value, unsigned digits, bool upper); + +/* ---- little-endian decoding ---- */ + +static inline uint16_t dmblkid_le16(const uint8_t* p) +{ + return (uint16_t)(p[0] | (p[1] << 8)); +} + +static inline uint32_t dmblkid_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); +} + +static inline uint64_t dmblkid_le64(const uint8_t* p) +{ + return (uint64_t)dmblkid_le32(p) | ((uint64_t)dmblkid_le32(p + 4) << 32); +} + +static inline bool dmblkid_bytes_equal(const uint8_t* a, const char* b, size_t size) +{ + for (size_t i = 0; i < size; i++) + { + if (a[i] != (uint8_t)b[i]) + { + return false; + } + } + return true; +} + +static inline bool dmblkid_is_power_of_2(uint32_t value) +{ + return value != 0 && (value & (value - 1u)) == 0; +} + +static inline bool dmblkid_has_boot_signature(const uint8_t* sector) +{ + return sector[510] == 0x55u && sector[511] == 0xAAu; +} + +#endif // DMBLKID_INTERNAL_H diff --git a/src/dmblkid_ptable.c b/src/dmblkid_ptable.c new file mode 100644 index 0000000..96b10a2 --- /dev/null +++ b/src/dmblkid_ptable.c @@ -0,0 +1,101 @@ +#include "dmblkid_internal.h" +#include "dmpart.h" +#include + +/* + * Partition tables are found by dmpart - the parser dmdevfs uses to create + * partition nodes - so a medium dmdevfs splits into partitions is reported + * here as a partition table, never as a filesystem to mount whole. + */ + +/* + * dmpart_read_t over a dmblkid_source_t. Static on purpose: the + * address of an external function is loaded through .got, which x86_64 ld + * relaxes into an absolute constant the module loader never relocates. + */ +static int source_part_read(void* ctx, uint64_t offset, void* buffer, size_t size) +{ + return dmblkid_source_read((const dmblkid_source_t*)ctx, offset, buffer, size); +} + +static bool gpt_signature_at(const dmblkid_source_t* source, uint64_t offset) +{ + uint8_t signature[8]; + return dmblkid_source_read(source, offset, signature, sizeof(signature)) == 0 && + dmblkid_bytes_equal(signature, "EFI PART", sizeof(signature)); +} + +/* + * Nodes that answer DMDRVI_IOCTL_BLOCK_GET_INFO give their block size. For + * anything else: the size whose primary (LBA 1) or backup (last LBA) GPT + * header is where it should be, 512 otherwise. + */ +static uint32_t pick_block_size(const dmblkid_source_t* source) +{ + if (source->block_size != 0) + { + return source->block_size; + } + for (uint32_t size = DMBLKID_SECTOR_SIZE; size <= DMBLKID_MAX_BLOCK_SIZE; size *= 2u) + { + uint64_t last = (source->size / size > 1u) ? (source->size / size - 1u) * size : 0; + if (gpt_signature_at(source, size) || (last != 0 && gpt_signature_at(source, last))) + { + return size; + } + } + return DMBLKID_SECTOR_SIZE; +} + +/* PTUUID: the MBR disk signature as 8 hex digits, the GPT disk GUID in the usual text form. */ +static int set_table_id(dmblkid_t* result, dmpart_table_t table, const uint8_t* id) +{ + char text[37]; + if (table == dmpart_table_mbr) + { + uint32_t signature = dmblkid_le32(id); + if (signature == 0) + { + return 0; + } + dmblkid_hex(text, signature, 8, false); + text[8] = '\0'; + return dmblkid_set_string(&result->uuid, text); + } + dmblkid_hex(text, dmblkid_le32(id), 8, false); + text[8] = '-'; + dmblkid_hex(text + 9, dmblkid_le16(id + 4), 4, false); + text[13] = '-'; + dmblkid_hex(text + 14, dmblkid_le16(id + 6), 4, false); + text[18] = '-'; + char* out = text + 19; + for (int i = 8; i < 16; i++) + { + dmblkid_hex(out, id[i], 2, false); + out += 2; + if (i == 9) + { + *out++ = '-'; + } + } + *out = '\0'; + return dmblkid_set_string(&result->uuid, text); +} + +int dmblkid_probe_ptable(const dmblkid_source_t* source, dmblkid_t* result) +{ + dmpart_medium_t medium = { source_part_read, (void*)source, pick_block_size(source), 0 }; + medium.block_count = source->size / medium.block_size; + dmpart_info_t info; + int ret = dmpart_scan(&medium, NULL, NULL, &info); + if (ret == -EINVAL || info.table == dmpart_table_none) + { + return (ret == -EINVAL) ? 0 : ret; /* -EINVAL: too small to hold a table */ + } + result->usage = dmblkid_usage_partition_table; + result->type = (info.table == dmpart_table_gpt) ? "gpt" : "mbr"; + result->block_size = medium.block_size; + result->partition_count = info.count; + ret = set_table_id(result, info.table, info.disk_id); + return (ret < 0) ? ret : 1; +} diff --git a/src/dmblkid_source.c b/src/dmblkid_source.c new file mode 100644 index 0000000..e79a51e --- /dev/null +++ b/src/dmblkid_source.c @@ -0,0 +1,59 @@ +#include "dmblkid_internal.h" +#include "dmdrvi_ioctl.h" +#include + +/* + * The node is read through the regular file API (Dmod_File*, 64-bit + * offsets) - on target that is dmvfs, so dmdevfs block and partition nodes + * work the same as image files on the host. It is only ever opened + * read-only. + */ + +/* Block nodes know their geometry; image files (and other nodes) fall back to the file size. */ +static void read_geometry(dmblkid_source_t* source) +{ + dmdrvi_block_info_t info = { 0 }; + if (Dmod_Ioctl(source->file, DMDRVI_IOCTL_BLOCK_GET_INFO, &info) == 0 && + dmblkid_is_power_of_2(info.logical_block_size) && info.block_count != 0 && + info.block_count <= UINT64_MAX / info.logical_block_size) + { + source->block_size = info.logical_block_size; + source->size = (uint64_t)info.block_count * info.logical_block_size; + return; + } + source->block_size = 0; + source->size = Dmod_FileSize(source->file); +} + +int dmblkid_source_open(dmblkid_source_t* source, const char* path) +{ + source->file = Dmod_FileOpen(path, "rb"); + if (source->file == NULL) + { + return -ENOENT; + } + read_geometry(source); + return 0; +} + +void dmblkid_source_close(dmblkid_source_t* source) +{ + if (source->file != NULL) + { + Dmod_FileClose(source->file); + source->file = NULL; + } +} + +int dmblkid_source_read(const dmblkid_source_t* source, uint64_t offset, void* buffer, size_t size) +{ + if (source->size != 0 && (offset > source->size || size > source->size - offset)) + { + return -ERANGE; + } + if (offset > (uint64_t)INT64_MAX || Dmod_FileSeek(source->file, (Dmod_FileOffset_t)offset, DMOD_SEEK_SET) != 0) + { + return -EIO; + } + return (Dmod_FileRead(buffer, 1, size, source->file) == size) ? 0 : -EIO; +} diff --git a/src/dmblkid_util.c b/src/dmblkid_util.c new file mode 100644 index 0000000..ceaaf13 --- /dev/null +++ b/src/dmblkid_util.c @@ -0,0 +1,117 @@ +#include "dmblkid_internal.h" +#include +#include + +void dmblkid_set_filesystem(dmblkid_t* result, const char* type, const char* module, bool mountable) +{ + result->usage = dmblkid_usage_filesystem; + result->type = type; + result->module = module; + result->mountable = mountable && module != NULL; +} + +int dmblkid_set_string(char** field, const char* value) +{ + char* copy = Dmod_StrDup(value); + if (copy == NULL) + { + return -ENOMEM; + } + Dmod_Free(*field); + *field = copy; + return 0; +} + +void dmblkid_hex(char* out, uint64_t value, unsigned digits, bool upper) +{ + char ten = upper ? 'A' : 'a'; + for (unsigned i = 0; i < digits; i++) + { + unsigned nibble = (unsigned)(value >> (4u * (digits - 1u - i))) & 0xFu; + out[i] = (char)((nibble < 10u) ? ('0' + nibble) : (ten + nibble - 10u)); + } +} + +int dmblkid_set_serial(char** field, uint32_t serial) +{ + char text[10]; + dmblkid_hex(text, serial >> 16, 4, true); + text[4] = '-'; + dmblkid_hex(text + 5, serial & 0xFFFFu, 4, true); + text[9] = '\0'; + return dmblkid_set_string(field, text); +} + +int dmblkid_set_label8(char** field, const uint8_t* label, size_t size) +{ + while (size > 0 && (label[size - 1] == ' ' || label[size - 1] == '\0')) + { + size--; + } + if (size == 0) + { + return 0; + } + char* text = Dmod_Malloc(size + 1u); + if (text == NULL) + { + return -ENOMEM; + } + for (size_t i = 0; i < size; i++) + { + text[i] = (label[i] >= 0x20u && label[i] < 0x7Fu) ? (char)label[i] : '?'; + } + text[size] = '\0'; + Dmod_Free(*field); + *field = text; + return 0; +} + +/* Append one BMP code point as UTF-8; returns the number of bytes written (1-3). */ +static size_t put_utf8(char* out, uint32_t code) +{ + if (code < 0x80u) + { + out[0] = (char)code; + return 1; + } + if (code < 0x800u) + { + out[0] = (char)(0xC0u | (code >> 6)); + out[1] = (char)(0x80u | (code & 0x3Fu)); + return 2; + } + out[0] = (char)(0xE0u | (code >> 12)); + out[1] = (char)(0x80u | ((code >> 6) & 0x3Fu)); + out[2] = (char)(0x80u | (code & 0x3Fu)); + return 3; +} + +int dmblkid_set_label16(char** field, const uint8_t* label, size_t units) +{ + char* text = Dmod_Malloc(units * 3u + 1u); + if (text == NULL) + { + return -ENOMEM; + } + size_t length = 0; + for (size_t i = 0; i < units; i++) + { + uint32_t code = dmblkid_le16(label + 2u * i); + if (code == 0) + { + break; + } + bool printable = code >= 0x20u && code != 0x7Fu && (code < 0xD800u || code > 0xDFFFu); + length += put_utf8(text + length, printable ? code : '?'); + } + text[length] = '\0'; + if (length == 0) + { + Dmod_Free(text); + return 0; + } + Dmod_Free(*field); + *field = text; + return 0; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c30c20f..398f7cc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,11 +1,12 @@ set(DMOD_MODULE_NAME test_dmblkid) set(DMOD_MODULE_VERSION ${PROJECT_VERSION}) set(DMOD_AUTHOR_NAME "Patryk Kubiak") -set(DMOD_STACK_SIZE 1024) +set(DMOD_STACK_SIZE 4096) set(DMOD_PRIORITY 0) dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} dmblkid_test.c + test_images.c ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE diff --git a/tests/dmblkid_test.c b/tests/dmblkid_test.c index 43b869d..53e2ea5 100644 --- a/tests/dmblkid_test.c +++ b/tests/dmblkid_test.c @@ -1,32 +1,252 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmod_test.h" #include "dmblkid.h" +#include "test_images.h" +#include +#include -static dmblkid_t g_handle = NULL; +/* + * Every step builds a (sparse) image file, probes it through dmblkid_probe() + * - the same file API path a /dev node takes on target - and checks the + * result. The image is removed in the teardown. + */ + +#define IMAGE_PATH "dmblkid_test.img" + +static dmblkid_t* g_result = NULL; void dmod_test_setup(void) { - g_handle = dmblkid_create(); + g_result = NULL; } void dmod_test_teardown(void) { - dmblkid_destroy(g_handle); - g_handle = NULL; + dmblkid_destroy(g_result); + g_result = NULL; + Dmod_FileRemove(IMAGE_PATH); +} + +static bool str_is(const char* value, const char* expected) +{ + return value != NULL && strcmp(value, expected) == 0; +} + +/* Probe IMAGE_PATH into g_result; true on success. */ +static bool probe(void) +{ + return dmblkid_probe(IMAGE_PATH, &g_result) == 0 && g_result != NULL; +} + +static void expect_filesystem(const char* type, const char* version, const char* label, const char* uuid) +{ + DMOD_TEST_EXPECT_EQ(dmblkid_get_usage(g_result), dmblkid_usage_filesystem); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_type(g_result), type)); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_version(g_result), version)); + DMOD_TEST_EXPECT_TRUE(label == NULL ? dmblkid_get_label(g_result) == NULL + : str_is(dmblkid_get_label(g_result), label)); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_uuid(g_result), uuid)); + DMOD_TEST_EXPECT_EQ(dmblkid_get_partition_count(g_result), 0u); +} + +static void expect_unknown(void) +{ + DMOD_TEST_EXPECT_EQ(dmblkid_get_usage(g_result), dmblkid_usage_unknown); + DMOD_TEST_EXPECT_NULL(dmblkid_get_type(g_result)); + DMOD_TEST_EXPECT_NULL(dmblkid_get_module(g_result)); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(g_result)); +} + +/* ---- filesystems ---- */ + +DMOD_TEST_STEP(fat12_floppy_with_mbr_like_boot_code) +{ + DMOD_TEST_EXPECT_TRUE(test_build_fat12(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_filesystem("vfat", "FAT12", "FLOPPY", "1234-ABCD"); + DMOD_TEST_EXPECT_EQ(dmblkid_get_size(g_result), 2880ull * 512u); + DMOD_TEST_EXPECT_NULL(dmblkid_get_module(g_result)); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(g_result)); } -DMOD_TEST_STEP(dmblkid_create) +DMOD_TEST_STEP(fat16_label_from_bpb) +{ + DMOD_TEST_EXPECT_TRUE(test_build_fat16(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_filesystem("vfat", "FAT16", "DATA16", "0BAD-F00D"); +} + +DMOD_TEST_STEP(fat32_label_from_root_directory) +{ + DMOD_TEST_EXPECT_TRUE(test_build_fat32(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_filesystem("vfat", "FAT32", "BOOT", "CAFE-BABE"); +} + +DMOD_TEST_STEP(exfat_above_4gib) +{ + DMOD_TEST_EXPECT_TRUE(test_build_exfat(IMAGE_PATH, true)); + DMOD_TEST_EXPECT_TRUE(probe()); + /* The label lives in the cluster heap at 4.5 GiB. */ + expect_filesystem("exfat", "1.0", "Karta \xC5\x82", "5EED-0001"); + DMOD_TEST_EXPECT_EQ(dmblkid_get_size(g_result), 5ull * TEST_GIB); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(g_result)); +} + +DMOD_TEST_STEP(exfat_bad_boot_checksum) +{ + DMOD_TEST_EXPECT_TRUE(test_build_exfat(IMAGE_PATH, false)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_unknown(); +} + +DMOD_TEST_STEP(dmffs_image) +{ + DMOD_TEST_EXPECT_TRUE(test_build_dmffs(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(probe()); + DMOD_TEST_EXPECT_EQ(dmblkid_get_usage(g_result), dmblkid_usage_filesystem); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_type(g_result), "dmffs")); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_version(g_result), "1.0")); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_module(g_result), "dmffs")); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(g_result)); /* dmffs mounts flash, not nodes */ + DMOD_TEST_EXPECT_NULL(dmblkid_get_label(g_result)); +} + +DMOD_TEST_STEP(dmffs_broken_chain) +{ + uint8_t length[4] = { 0xFF, 0xFF, 0xFF, 0x00 }; /* FILE TLV running past the end */ + DMOD_TEST_EXPECT_TRUE(test_build_dmffs(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(test_image_write(IMAGE_PATH, 15, length, sizeof(length))); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_unknown(); +} + +/* ---- partition tables ---- */ + +DMOD_TEST_STEP(mbr_with_logical_partition) +{ + DMOD_TEST_EXPECT_TRUE(test_build_mbr(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(probe()); + DMOD_TEST_EXPECT_EQ(dmblkid_get_usage(g_result), dmblkid_usage_partition_table); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_type(g_result), "mbr")); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_uuid(g_result), "1a2b3c4d")); + DMOD_TEST_EXPECT_EQ(dmblkid_get_partition_count(g_result), 2u); + DMOD_TEST_EXPECT_EQ(dmblkid_get_block_size(g_result), 512u); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(g_result)); +} + +static void expect_gpt(uint32_t block_size) +{ + DMOD_TEST_EXPECT_EQ(dmblkid_get_usage(g_result), dmblkid_usage_partition_table); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_type(g_result), "gpt")); + DMOD_TEST_EXPECT_TRUE(str_is(dmblkid_get_uuid(g_result), "01234567-89ab-cdef-0011-223344556677")); + DMOD_TEST_EXPECT_EQ(dmblkid_get_partition_count(g_result), 2u); + DMOD_TEST_EXPECT_EQ(dmblkid_get_block_size(g_result), block_size); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(g_result)); +} + +DMOD_TEST_STEP(gpt_disk) +{ + DMOD_TEST_EXPECT_TRUE(test_build_gpt(IMAGE_PATH, 64u * TEST_MIB, 512, false)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_gpt(512); +} + +DMOD_TEST_STEP(gpt_backup_header_above_4gib) +{ + DMOD_TEST_EXPECT_TRUE(test_build_gpt(IMAGE_PATH, 6u * TEST_GIB, 512, true)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_gpt(512); + DMOD_TEST_EXPECT_EQ(dmblkid_get_size(g_result), 6ull * TEST_GIB); +} + +DMOD_TEST_STEP(gpt_4k_blocks) +{ + DMOD_TEST_EXPECT_TRUE(test_build_gpt(IMAGE_PATH, 64u * TEST_MIB, 4096, false)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_gpt(4096); +} + +/* ---- nothing to find ---- */ + +DMOD_TEST_STEP(blank_device) +{ + DMOD_TEST_EXPECT_TRUE(test_image_create(IMAGE_PATH, TEST_MIB)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_unknown(); + DMOD_TEST_EXPECT_NULL(dmblkid_get_uuid(g_result)); + DMOD_TEST_EXPECT_EQ(dmblkid_get_size(g_result), TEST_MIB); +} + +DMOD_TEST_STEP(tiny_file) +{ + DMOD_TEST_EXPECT_TRUE(test_image_create(IMAGE_PATH, 100)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_unknown(); +} + +DMOD_TEST_STEP(empty_file) +{ + DMOD_TEST_EXPECT_TRUE(test_image_create(IMAGE_PATH, 0)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_unknown(); + DMOD_TEST_EXPECT_EQ(dmblkid_get_size(g_result), 0u); +} + +/* Build the FAT32 image, overwrite @p size bytes at @p offset, expect nothing recognized. */ +static void expect_damaged_fat32(uint32_t offset, const void* data, size_t size) +{ + DMOD_TEST_EXPECT_TRUE(test_build_fat32(IMAGE_PATH)); + DMOD_TEST_EXPECT_TRUE(test_image_write(IMAGE_PATH, offset, data, size)); + DMOD_TEST_EXPECT_TRUE(probe()); + expect_unknown(); + dmblkid_destroy(g_result); + g_result = NULL; +} + +DMOD_TEST_STEP(damaged_boot_sector) +{ + uint8_t zero[2] = { 0, 0 }; + uint8_t bps[2] = { 0x00, 0x03 }; /* 768: not a power of 2 */ + uint8_t fat_size[4] = { 0x10, 0, 0, 0 }; /* 16 sectors cannot map 129k clusters */ + uint8_t total[4] = { 0x00, 0x00, 0x04, 0x00 }; /* 256k sectors: more than the 64 MiB node */ + expect_damaged_fat32(510, zero, sizeof(zero)); /* no 0x55AA */ + expect_damaged_fat32(11, bps, sizeof(bps)); + expect_damaged_fat32(36, fat_size, sizeof(fat_size)); + expect_damaged_fat32(32, total, sizeof(total)); + expect_damaged_fat32(0, zero, sizeof(zero)); /* no jump instruction */ +} + +/* ---- API contract ---- */ + +DMOD_TEST_STEP(probe_never_writes) { - DMOD_TEST_EXPECT_NOT_NULL(g_handle); + DMOD_TEST_EXPECT_TRUE(test_build_fat32(IMAGE_PATH)); + uint64_t checked = 2200u * 512u; /* boot sector, FATs, root directory */ + uint32_t before = test_image_checksum(IMAGE_PATH, checked); + DMOD_TEST_EXPECT_TRUE(probe()); + DMOD_TEST_EXPECT_EQ(test_image_checksum(IMAGE_PATH, checked), before); + DMOD_TEST_EXPECT_NE(before, 0u); } -DMOD_TEST_STEP(dmblkid_is_valid) +DMOD_TEST_STEP(bad_arguments) { - DMOD_TEST_EXPECT_TRUE(dmblkid_is_valid(g_handle)); + dmblkid_t* result = (dmblkid_t*)&result; + DMOD_TEST_EXPECT_EQ(dmblkid_probe(NULL, &result), -EINVAL); + DMOD_TEST_EXPECT_EQ(dmblkid_probe(IMAGE_PATH, NULL), -EINVAL); + DMOD_TEST_EXPECT_EQ(dmblkid_probe("dmblkid_no_such_node.img", &result), -ENOENT); + DMOD_TEST_EXPECT_NULL(result); } -DMOD_TEST_STEP(dmblkid_destroy_null) +DMOD_TEST_STEP(accessors_reject_invalid_results) { - /* Destroying NULL must not crash. */ + uint32_t garbage[16] = { 0 }; + const dmblkid_t* bogus = (const dmblkid_t*)garbage; + DMOD_TEST_EXPECT_EQ(dmblkid_get_usage(NULL), dmblkid_usage_unknown); + DMOD_TEST_EXPECT_NULL(dmblkid_get_type(bogus)); + DMOD_TEST_EXPECT_NULL(dmblkid_get_label(bogus)); + DMOD_TEST_EXPECT_EQ(dmblkid_get_size(bogus), 0u); + DMOD_TEST_EXPECT_FALSE(dmblkid_is_mountable(bogus)); dmblkid_destroy(NULL); + dmblkid_destroy((dmblkid_t*)garbage); /* ignored: no magic */ } diff --git a/tests/test_images.c b/tests/test_images.c new file mode 100644 index 0000000..70b1ffa --- /dev/null +++ b/tests/test_images.c @@ -0,0 +1,407 @@ +#include "dmod.h" +#include "test_images.h" +#include + +#define SECTOR 512u + +/* ---- helpers ---- */ + +static void put16(uint8_t* p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); } +static void put32(uint8_t* p, uint32_t v) { put16(p, (uint16_t)v); put16(p + 2, (uint16_t)(v >> 16)); } +static void put64(uint8_t* p, uint64_t v) { put32(p, (uint32_t)v); put32(p + 4, (uint32_t)(v >> 32)); } + +static uint8_t* zeroed(size_t size) +{ + uint8_t* buffer = Dmod_Malloc(size); + if (buffer != NULL) + { + memset(buffer, 0, size); + } + return buffer; +} + +static bool write_and_free(const char* path, uint64_t offset, uint8_t* data, size_t size) +{ + bool ok = data != NULL && test_image_write(path, offset, data, size); + Dmod_Free(data); + return ok; +} + +static uint32_t crc32(const uint8_t* data, size_t size) +{ + uint32_t crc = 0xFFFFFFFFu; + for (size_t i = 0; i < size; i++) + { + crc ^= data[i]; + for (int bit = 0; bit < 8; bit++) + { + crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); + } + } + return ~crc; +} + +/* ---- raw file access ---- */ + +static bool seek_and(void* file, uint64_t offset) +{ + return file != NULL && Dmod_FileSeek(file, (Dmod_FileOffset_t)offset, DMOD_SEEK_SET) == 0; +} + +bool test_image_create(const char* path, uint64_t size) +{ + uint8_t zero = 0; + void* file = Dmod_FileOpen(path, "wb"); + bool ok = size == 0 || (seek_and(file, size - 1u) && Dmod_FileWrite(&zero, 1, 1, file) == 1); + if (file != NULL) + { + Dmod_FileClose(file); + } + return file != NULL && ok; +} + +bool test_image_write(const char* path, uint64_t offset, const void* data, size_t size) +{ + void* file = Dmod_FileOpen(path, "r+b"); + bool ok = seek_and(file, offset) && Dmod_FileWrite(data, 1, size, file) == size; + if (file != NULL) + { + Dmod_FileClose(file); + } + return ok; +} + +bool test_image_read(const char* path, uint64_t offset, void* data, size_t size) +{ + void* file = Dmod_FileOpen(path, "rb"); + bool ok = seek_and(file, offset) && Dmod_FileRead(data, 1, size, file) == size; + if (file != NULL) + { + Dmod_FileClose(file); + } + return ok; +} + +uint32_t test_image_checksum(const char* path, uint64_t size) +{ + uint8_t block[SECTOR]; + uint32_t sum = 0; + for (uint64_t offset = 0; offset < size; offset += SECTOR) + { + if (!test_image_read(path, offset, block, SECTOR)) + { + return 0; + } + sum = crc32(block, SECTOR) ^ ((sum << 1) | (sum >> 31)); + } + return sum; +} + +/* ---- FAT ---- */ + +typedef struct +{ + uint16_t reserved; + uint8_t sectors_per_cluster; + uint16_t root_entries; + uint32_t total_sectors; + uint32_t fat_size; + uint8_t media; + bool fat32; + uint32_t serial; + const char* label; /* 11 characters */ + const char* fs_name; /* 8 characters */ +} fat_params_t; + +static uint8_t* fat_boot_sector(const fat_params_t* p) +{ + uint8_t* s = zeroed(SECTOR); + if (s == NULL) + { + return NULL; + } + s[0] = 0xEB; s[1] = 0x3C; s[2] = 0x90; + memcpy(s + 3, "MSWIN4.1", 8); + put16(s + 11, SECTOR); + s[13] = p->sectors_per_cluster; + put16(s + 14, p->reserved); + s[16] = 2; + put16(s + 17, p->root_entries); + put16(s + 19, (p->total_sectors < 0x10000u && !p->fat32) ? (uint16_t)p->total_sectors : 0); + s[21] = p->media; + put16(s + 22, p->fat32 ? 0 : (uint16_t)p->fat_size); + put32(s + 32, (p->total_sectors < 0x10000u && !p->fat32) ? 0 : p->total_sectors); + uint8_t* ext = s + (p->fat32 ? 64 : 36); + if (p->fat32) + { + put32(s + 36, p->fat_size); + put32(s + 44, 2); /* root cluster */ + } + ext[0] = 0x80; + ext[2] = 0x29; + put32(ext + 3, p->serial); + memcpy(ext + 7, p->label, 11); + memcpy(ext + 18, p->fs_name, 8); + s[510] = 0x55; s[511] = 0xAA; + return s; +} + +static bool write_dir_entry(const char* path, uint64_t offset, uint8_t first, const char* name, uint8_t attr) +{ + uint8_t entry[32] = { 0 }; + memcpy(entry, name, 11); + entry[0] = first; + entry[11] = attr; + return test_image_write(path, offset, entry, sizeof(entry)); +} + +bool test_build_fat12(const char* path) +{ + fat_params_t p = { 1, 1, 224, 2880, 9, 0xF0, false, 0x1234ABCDu, "NO NAME ", "FAT12 " }; + uint8_t* s = fat_boot_sector(&p); + if (s != NULL) + { + /* Boot code that looks like a bootable partition entry - still not an MBR. */ + s[446] = 0x80; s[450] = 0x06; put32(s + 454, 63); put32(s + 458, 2000); + } + uint64_t root = (1u + 2u * 9u) * SECTOR; + return test_image_create(path, 2880u * SECTOR) && write_and_free(path, 0, s, SECTOR) && + write_dir_entry(path, root, 'F', "FLOPPY ", 0x08); +} + +bool test_build_fat16(const char* path) +{ + fat_params_t p = { 4, 4, 512, 65536, 64, 0xF8, false, 0x0BADF00Du, "DATA16 ", "FAT16 " }; + return test_image_create(path, 32u * TEST_MIB) && write_and_free(path, 0, fat_boot_sector(&p), SECTOR); +} + +bool test_build_fat32(const char* path) +{ + fat_params_t p = { 32, 1, 0, 131072, 1024, 0xF8, true, 0xCAFEBABEu, "OLDLABEL ", "FAT32 " }; + uint64_t root = (32u + 2u * 1024u) * SECTOR; /* first data sector = cluster 2 */ + return test_image_create(path, 64u * TEST_MIB) && write_and_free(path, 0, fat_boot_sector(&p), SECTOR) && + write_dir_entry(path, root, 0xE5, "GONE ", 0x08) && + write_dir_entry(path, root + 32u, 'A', "LONGNAMEPAR", 0x0F) && + write_dir_entry(path, root + 64u, 'B', "BOOT ", 0x28); +} + +/* ---- exFAT ---- */ + +#define EXFAT_VOLUME_SECTORS (5ull * TEST_GIB / SECTOR) +#define EXFAT_HEAP_OFFSET 9437184u /* 4.5 GiB */ +#define EXFAT_CLUSTER_SHIFT 3u +#define EXFAT_ROOT_CLUSTER 5u + +static void exfat_boot_sector(uint8_t* s) +{ + s[0] = 0xEB; s[1] = 0x76; s[2] = 0x90; + memcpy(s + 3, "EXFAT ", 8); + put64(s + 72, EXFAT_VOLUME_SECTORS); + put32(s + 80, 2048); /* FAT offset */ + put32(s + 84, 1025); /* FAT length */ + put32(s + 88, EXFAT_HEAP_OFFSET); + put32(s + 92, 131072); /* cluster count */ + put32(s + 96, EXFAT_ROOT_CLUSTER); + put32(s + 100, 0x5EED0001u); + s[104] = 0x00; s[105] = 0x01; /* revision 1.00 */ + s[106] = 0x02; /* VolumeFlags: excluded from the checksum */ + s[108] = 9; + s[109] = EXFAT_CLUSTER_SHIFT; + s[110] = 1; + s[111] = 0x80; + s[112] = 37; /* PercentInUse: excluded from the checksum */ + s[510] = 0x55; s[511] = 0xAA; +} + +/* Sectors 0-11 of the boot region, sector 11 holding the checksum. */ +static uint8_t* exfat_boot_region(bool valid_checksum) +{ + uint8_t* region = zeroed(12u * SECTOR); + if (region == NULL) + { + return NULL; + } + exfat_boot_sector(region); + for (uint32_t i = 1; i <= 8; i++) + { + put32(region + i * SECTOR + 508, 0xAA550000u); /* extended boot sectors */ + } + uint32_t checksum = 0; + for (uint32_t i = 0; i < 11u * SECTOR; i++) + { + if (i != 106 && i != 107 && i != 112) + { + checksum = ((checksum & 1u) ? 0x80000000u : 0u) + (checksum >> 1) + region[i]; + } + } + for (uint32_t i = 0; i < SECTOR; i += 4) + { + put32(region + 11u * SECTOR + i, valid_checksum ? checksum : checksum + 1u); + } + return region; +} + +bool test_build_exfat(const char* path, bool valid_checksum) +{ + static const uint16_t label[] = { 'K', 'a', 'r', 't', 'a', ' ', 0x0142 }; + uint8_t entries[64] = { 0 }; + entries[0] = 0x81; /* allocation bitmap */ + entries[32] = 0x83; /* volume label */ + entries[33] = 7; + for (int i = 0; i < 7; i++) + { + put16(entries + 34 + 2 * i, label[i]); + } + uint64_t root = ((uint64_t)EXFAT_HEAP_OFFSET + ((EXFAT_ROOT_CLUSTER - 2u) << EXFAT_CLUSTER_SHIFT)) * SECTOR; + return test_image_create(path, 5u * TEST_GIB) && + write_and_free(path, 0, exfat_boot_region(valid_checksum), 12u * SECTOR) && + test_image_write(path, root, entries, sizeof(entries)); +} + +/* ---- dmffs ---- */ + +static size_t put_tlv(uint8_t* p, uint32_t type, const char* value, uint32_t length) +{ + put32(p, type); + put32(p + 4, length); + if (value != NULL) + { + memcpy(p + 8, value, length); + } + return 8u + (value != NULL ? length : 0u); +} + +bool test_build_dmffs(const char* path) +{ + uint8_t image[96] = { 0 }; + size_t size = put_tlv(image, 3, "1.0", 3); + size += put_tlv(image + size, 1, NULL, 23); /* FILE: NAME + DATA below */ + size += put_tlv(image + size, 4, "a.txt", 5); + size += put_tlv(image + size, 5, "hi", 2); + size += put_tlv(image + size, 2, NULL, 9); /* DIR: NAME below */ + size += put_tlv(image + size, 4, "d", 1); + size += put_tlv(image + size, 0xFFFFFFFFu, NULL, 0); + return test_image_create(path, size) && test_image_write(path, 0, image, size); +} + +/* ---- MBR ---- */ + +static void mbr_entry(uint8_t* sector, int index, uint8_t status, uint8_t type, uint32_t first, uint32_t count) +{ + uint8_t* e = sector + 446 + 16 * index; + e[0] = status; + e[4] = type; + put32(e + 8, first); + put32(e + 12, count); +} + +bool test_build_mbr(const char* path) +{ + uint8_t* mbr = zeroed(SECTOR); + uint8_t* ebr = zeroed(SECTOR); + if (mbr != NULL && ebr != NULL) + { + put32(mbr + 440, 0x1A2B3C4Du); + mbr_entry(mbr, 0, 0x80, 0x0C, 2048, 65536); + mbr_entry(mbr, 1, 0x00, 0x05, 67584, 32768); + mbr[510] = 0x55; mbr[511] = 0xAA; + mbr_entry(ebr, 0, 0x00, 0x0C, 2048, 8192); + ebr[510] = 0x55; ebr[511] = 0xAA; + } + bool ok = test_image_create(path, 64u * TEST_MIB); + ok = write_and_free(path, 0, mbr, SECTOR) && ok; + return write_and_free(path, 67584ull * SECTOR, ebr, SECTOR) && ok; +} + +/* ---- GPT ---- */ + +#define GPT_ENTRIES 128u +#define GPT_ENTRY_SIZE 128u +#define GPT_ARRAY_BYTES (GPT_ENTRIES * GPT_ENTRY_SIZE) + +typedef struct +{ + uint32_t block_size; + uint64_t last_lba; + uint64_t entry_blocks; + uint64_t first_usable; + uint64_t last_usable; +} gpt_layout_t; + +static void gpt_part(uint8_t* e, uint8_t id, uint64_t first, uint64_t last) +{ + memset(e, id, 16); /* type GUID */ + memset(e + 16, id + 1, 16); /* unique GUID */ + put64(e + 32, first); + put64(e + 40, last); +} + +static uint8_t* gpt_header(const gpt_layout_t* l, uint64_t my_lba, uint64_t other_lba, uint64_t entries_lba, + uint32_t entries_crc) +{ + static const uint8_t guid[16] = { 0x67, 0x45, 0x23, 0x01, 0xAB, 0x89, 0xEF, 0xCD, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 }; + uint8_t* h = zeroed(l->block_size); + if (h == NULL) + { + return NULL; + } + memcpy(h, "EFI PART", 8); + put32(h + 8, 0x00010000u); + put32(h + 12, 92); + put64(h + 24, my_lba); + put64(h + 32, other_lba); + put64(h + 40, l->first_usable); + put64(h + 48, l->last_usable); + memcpy(h + 56, guid, 16); + put64(h + 72, entries_lba); + put32(h + 80, GPT_ENTRIES); + put32(h + 84, GPT_ENTRY_SIZE); + put32(h + 88, entries_crc); + put32(h + 16, crc32(h, 92)); + return h; +} + +static bool gpt_write_tables(const char* path, const gpt_layout_t* l, bool damage_primary) +{ + uint8_t* entries = zeroed(GPT_ARRAY_BYTES); + if (entries == NULL) + { + return false; + } + gpt_part(entries, 0x11, l->first_usable, l->first_usable + 1000u); + gpt_part(entries + GPT_ENTRY_SIZE, 0x22, l->first_usable + 2000u, l->last_usable); + uint32_t crc = crc32(entries, GPT_ARRAY_BYTES); + uint64_t backup_entries = l->last_lba - l->entry_blocks; + uint8_t* primary = gpt_header(l, 1, l->last_lba, 2, crc); + uint8_t* backup = gpt_header(l, l->last_lba, 1, backup_entries, crc); + if (primary != NULL && damage_primary) + { + primary[60] ^= 0xFFu; /* disk GUID changed after the CRC was computed */ + } + bool ok = test_image_write(path, 2ull * l->block_size, entries, GPT_ARRAY_BYTES) && + test_image_write(path, backup_entries * l->block_size, entries, GPT_ARRAY_BYTES); + Dmod_Free(entries); + ok = write_and_free(path, l->block_size, primary, l->block_size) && ok; + return write_and_free(path, l->last_lba * l->block_size, backup, l->block_size) && ok; +} + +bool test_build_gpt(const char* path, uint64_t size, uint32_t block_size, bool damage_primary) +{ + gpt_layout_t l; + l.block_size = block_size; + l.last_lba = size / block_size - 1u; + l.entry_blocks = GPT_ARRAY_BYTES / block_size; + l.first_usable = 2u + l.entry_blocks; + l.last_usable = l.last_lba - l.entry_blocks - 1u; + + uint8_t* mbr = zeroed(block_size); + if (mbr != NULL) + { + uint64_t blocks = size / block_size - 1u; + mbr_entry(mbr, 0, 0x00, 0xEE, 1, (blocks > 0xFFFFFFFFu) ? 0xFFFFFFFFu : (uint32_t)blocks); + mbr[510] = 0x55; mbr[511] = 0xAA; + } + return test_image_create(path, size) && write_and_free(path, 0, mbr, block_size) && + gpt_write_tables(path, &l, damage_primary); +} diff --git a/tests/test_images.h b/tests/test_images.h new file mode 100644 index 0000000..b682075 --- /dev/null +++ b/tests/test_images.h @@ -0,0 +1,64 @@ +/** + * @file test_images.h + * @brief Builders of the disk images the dmblkid tests probe + * + * Images are sparse files: only the structures a prober reads are written, + * so multi-GiB images cost a few sectors of disk space. + */ +#ifndef TEST_IMAGES_H +#define TEST_IMAGES_H + +#include +#include +#include + +#define TEST_MIB (1024ull * 1024ull) +#define TEST_GIB (1024ull * TEST_MIB) + +/* ---- raw file access ---- */ + +/** Create (truncate) @p path with @p size bytes of zeros. */ +bool test_image_create(const char* path, uint64_t size); + +/** Write @p size bytes at @p offset of an existing image. */ +bool test_image_write(const char* path, uint64_t offset, const void* data, size_t size); + +/** Read @p size bytes at @p offset. */ +bool test_image_read(const char* path, uint64_t offset, void* data, size_t size); + +/** A 32-bit checksum of the first @p size bytes - to prove probing did not write. */ +uint32_t test_image_checksum(const char* path, uint64_t size); + +/* ---- filesystems ---- */ + +/** 1.44 MB FAT12 floppy: label "FLOPPY" in the root directory, "NO NAME" in the BPB, serial 1234-ABCD. */ +bool test_build_fat12(const char* path); + +/** 32 MiB FAT16: label "DATA16" only in the BPB, serial 0BAD-F00D. */ +bool test_build_fat16(const char* path); + +/** 64 MiB FAT32: root directory label "BOOT" (after a deleted and an LFN entry), BPB "OLDLABEL", serial CAFE-BABE. */ +bool test_build_fat32(const char* path); + +/** + * 5 GiB exFAT with its cluster heap (and label "Karta ł") above 4 GiB, + * serial 5EED-0001, revision 1.0. @p valid_checksum false breaks the boot region checksum. + */ +bool test_build_exfat(const char* path, bool valid_checksum); + +/** dmffs image: VERSION "1.0", a FILE and a DIR, END. */ +bool test_build_dmffs(const char* path); + +/* ---- partition tables ---- */ + +/** 64 MiB MBR disk, signature 1a2b3c4d: primary 1 (FAT32 LBA) and one logical partition (5). */ +bool test_build_mbr(const char* path); + +/** + * GPT disk of @p size bytes with @p block_size blocks and two partitions. + * Disk GUID 01234567-89ab-cdef-0011-223344556677. @p damage_primary breaks + * the primary header so only the backup at the last LBA is valid. + */ +bool test_build_gpt(const char* path, uint64_t size, uint32_t block_size, bool damage_primary); + +#endif // TEST_IMAGES_H