Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
include/exfat_unpacker.h
zftpd / include / exfat_unpacker.h
1#ifndef EXFAT_UNPACKER_H
2#define EXFAT_UNPACKER_H
3/*
4 * exfat_unpacker — exFAT filesystem image parser
5 * Copyright (C) 2025 seregonwar
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 *
20 * exfat_unpacker.h — exFAT filesystem image parser
21 *
22 * Provides read-only access to exFAT images: boot sector, FAT chain,
23 * directory traversal and in-memory file extraction.
24 *
25 * THREAD SAFETY: NOT thread-safe. Callers that share an exfat_context_t
26 * across threads must provide external serialisation. In the NetMount
27 * server each call to ensure_meta() holds the per-export meta_mutex, so
28 * only one thread ever uses a context at a time — no additional locking
29 * is needed.
30 *
31 * BUG HISTORY (original library, fixed here):
32 * 1. exfat_boot_sector_t: volume_flags was uint32_t (should be uint16_t),
33 * shifting bytes_per_sector_power to 0x6E instead of the correct 0x6C.
34 * 2. exfat_file_entry_t: phantom first_cluster/data_length fields made
35 * the struct 44 bytes instead of 32; tenth-of-second fields were
36 * uint16_t instead of uint8_t.
37 * 3. exfat_filename_entry_t: name_length + name_hash overflowed beyond
38 * the 32-byte entry boundary, causing out-of-bounds reads.
39 * 4. exfat_read_directory: read only one cluster, silently truncating
40 * large directories that span multiple clusters.
41 * 5. exfat_extract_*: per-cluster malloc in hot path; NoFatChain flag
42 * (stream extension GeneralSecondaryFlags bit 1) ignored.
43 */
44 
45#include <stdint.h>
46#include <stddef.h>
47#include <stdio.h>
48#include <stdlib.h>
49#include <string.h>
50#include <time.h>
51#include <sys/types.h> /* ssize_t */
52#if defined(_MSC_VER)
53# include <BaseTsd.h>
54# ifndef ssize_t
55typedef SSIZE_T ssize_t;
56# endif
57#endif
58 
59#if defined(_MSC_VER)
60# pragma pack(push, 1)
61# define EXFAT_PACKED
62#else
63# define EXFAT_PACKED __attribute__((packed))
64#endif
65 
66#ifdef __cplusplus
67extern "C" {
68#endif
69 
70/* ── Constants ──────────────────────────────────────────────────────────── */
71 
72#define EXFAT_SECTOR_SIZE 512
73#define EXFAT_BOOT_SIGNATURE 0xAA55U
74#define EXFAT_FS_NAME "EXFAT " /* 8 bytes per spec */
75#define EXFAT_FS_NAME_LEN 8
76#define EXFAT_ENTRY_SIZE 32
77#define EXFAT_MAX_FILENAME_LEN 255 /* spec max: 255 chars */
78#define EXFAT_MAX_NAME_ENTRIES 17 /* ceil(255/15) */
79 
80/* Directory entry types */
81#define EXFAT_ENTRY_TYPE_FILE_DIR 0x85U
82#define EXFAT_ENTRY_TYPE_STREAM_EXT 0xC0U
83#define EXFAT_ENTRY_TYPE_FILENAME 0xC1U
84#define EXFAT_ENTRY_TYPE_BITMAP 0x81U
85#define EXFAT_ENTRY_TYPE_UPCASE 0x82U
86#define EXFAT_ENTRY_TYPE_VOLID 0x83U
87#define EXFAT_ENTRY_TYPE_EOD 0x00U /* end-of-directory */
88 
89/* GeneralSecondaryFlags (stream extension offset 0x01) */
90#define EXFAT_FLAG_ALLOC_POSSIBLE 0x01U
91#define EXFAT_FLAG_NO_FAT_CHAIN 0x02U /* contiguous clusters */
92 
93/* File attributes */
94#define EXFAT_ATTR_READ_ONLY 0x0001U
95#define EXFAT_ATTR_HIDDEN 0x0002U
96#define EXFAT_ATTR_SYSTEM 0x0004U
97#define EXFAT_ATTR_VOLUME_LABEL 0x0008U
98#define EXFAT_ATTR_DIRECTORY 0x0010U
99#define EXFAT_ATTR_ARCHIVE 0x0020U
100 
101/* FAT special values */
102#define EXFAT_FAT_FREE_CLUSTER 0x00000000U
103#define EXFAT_FAT_MEDIA_DESCRIPTOR 0xFFFFFFF8U
104#define EXFAT_FAT_END_OF_CHAIN 0xFFFFFFFFU
105#define EXFAT_FAT_BAD_CLUSTER 0xFFFFFFF7U
106 
107/* I/O chunk size for file extraction (avoids per-cluster malloc) */
108#define EXFAT_IO_CHUNK_SIZE (256U * 1024U) /* 256 KiB */
109 
110/* ── Boot Sector (512 bytes, Microsoft exFAT spec §3.1) ─────────────────── */
111/*
112 * Accurate field layout with correct offsets. Previously volume_flags was
113 * uint32_t (4 bytes) which shifted bytes_per_sector_power from 0x6C to 0x6E.
114 *
115 * Offset Field Size
116 * ────── ─────────────────────────── ────
117 * 0x00 JumpBoot 3
118 * 0x03 FileSystemName ("EXFAT ") 8
119 * 0x0B MustBeZero 53
120 * 0x40 PartitionOffset (uint64) 8
121 * 0x48 VolumeLength (uint64) 8
122 * 0x50 FatOffset (uint32) 4
123 * 0x54 FatLength (uint32) 4
124 * 0x58 ClusterHeapOffset (uint32) 4
125 * 0x5C ClusterCount (uint32) 4
126 * 0x60 FirstClusterOfRootDir (u32) 4
127 * 0x64 VolumeSerialNumber (uint32) 4
128 * 0x68 FileSystemRevision (uint16) 2
129 * 0x6A VolumeFlags (uint16) 2 ← was uint32_t (BUG)
130 * 0x6C BytesPerSectorShift (uint8) 1 ← was at 0x6E (BUG)
131 * 0x6D SectorsPerClusterShift (u8) 1 ← was at 0x6F (BUG)
132 * 0x6E NumberOfFats (uint8) 1
133 * 0x6F DriveSelect (uint8) 1
134 * 0x70 PercentInUse (uint8) 1
135 * 0x71 Reserved 7
136 * 0x78 BootCode 390
137 * 0x1FE BootSignature (uint16) 2
138 * ──────────────────────────────────────
139 * Total 512
140 */
141typedef struct EXFAT_PACKED {
142 uint8_t jump_boot[3]; /* 0x00 */
143 uint8_t fs_name[8]; /* 0x03 "EXFAT " */
144 uint8_t must_be_zero[53]; /* 0x0B */
145 uint64_t partition_offset; /* 0x40 sectors from media start */
146 uint64_t volume_length; /* 0x48 sectors in volume */
147 uint32_t fat_offset; /* 0x50 sectors from VBR start */
148 uint32_t fat_length; /* 0x54 sectors */
149 uint32_t cluster_heap_offset; /* 0x58 sectors from VBR start */
150 uint32_t total_clusters; /* 0x5C */
151 uint32_t root_dir_first_cluster; /* 0x60 */
152 uint32_t volume_serial; /* 0x64 */
153 uint16_t fs_revision; /* 0x68 e.g. 0x0100 = 1.00 */
154 uint16_t volume_flags; /* 0x6A FIXED: was uint32_t */
155 uint8_t bytes_per_sector_shift; /* 0x6C 2^n bytes/sector [9,12] */
156 uint8_t sectors_per_cluster_shift; /* 0x6D 2^n sectors/cluster */
157 uint8_t num_fats; /* 0x6E usually 1 */
158 uint8_t drive_select; /* 0x6F */
159 uint8_t percent_in_use; /* 0x70 0xFF = unknown */
160 uint8_t reserved[7]; /* 0x71 */
161 uint8_t boot_code[390]; /* 0x78 */
162 uint16_t boot_signature; /* 0x1FE must be 0xAA55 */
163} exfat_boot_sector_t;
164 
165/* Compile-time size assertion — catches any future struct changes. */
166typedef char exfat_boot_sector_size_check[
167 (sizeof(exfat_boot_sector_t) == 512) ? 1 : -1];
168 
169/* ── File Directory Entry (0x85, 32 bytes) ──────────────────────────────── */
170/*
171 * FIXED: removed phantom first_cluster / data_length fields that do not
172 * exist in the 0x85 entry (they belong to the stream extension 0xC0).
173 * Also fixed: tenth-of-second fields changed from uint16_t to uint8_t.
174 *
175 * 0x00 EntryType 1
176 * 0x01 SecondaryCount 1
177 * 0x02 SetChecksum 2
178 * 0x04 FileAttributes 2
179 * 0x06 Reserved1 2
180 * 0x08 CreateTimestamp 4
181 * 0x0C LastModTimestamp 4
182 * 0x10 LastAccTimestamp 4
183 * 0x14 Create10ms 1
184 * 0x15 LastMod10ms 1
185 * 0x16 CreateUtcOffset 1
186 * 0x17 LastModUtcOffset 1
187 * 0x18 LastAccUtcOffset 1
188 * 0x19 Reserved2 7
189 * ─────────────────────── ──
190 * 32
191 */
192typedef struct EXFAT_PACKED {
193 uint8_t entry_type; /* 0x00 0x85 */
194 uint8_t secondary_count; /* 0x01 number of secondary entries */
195 uint16_t set_checksum; /* 0x02 */
196 uint16_t file_attributes; /* 0x04 EXFAT_ATTR_* */
197 uint16_t reserved1; /* 0x06 */
198 uint32_t create_time; /* 0x08 DOS timestamp */
199 uint32_t last_modified_time; /* 0x0C DOS timestamp */
200 uint32_t last_accessed_time; /* 0x10 DOS timestamp */
201 uint8_t create_10ms; /* 0x14 FIXED: was uint16_t */
202 uint8_t last_mod_10ms; /* 0x15 FIXED: was uint16_t */
203 uint8_t create_utc_offset; /* 0x16 */
204 uint8_t last_mod_utc_offset;/* 0x17 */
205 uint8_t last_acc_utc_offset;/* 0x18 */
206 uint8_t reserved2[7]; /* 0x19 */
207} exfat_file_entry_t;
208 
209typedef char exfat_file_entry_size_check[
210 (sizeof(exfat_file_entry_t) == 32) ? 1 : -1];
211 
212/* ── Stream Extension Entry (0xC0, 32 bytes) ────────────────────────────── */
213/*
214 * 0x00 EntryType 1
215 * 0x01 GeneralSecondaryFlags 1 bit0=AllocPossible, bit1=NoFatChain
216 * 0x02 Reserved1 1
217 * 0x03 NameLength 1 filename length in chars (max 255)
218 * 0x04 NameHash 2
219 * 0x06 Reserved2 2
220 * 0x08 ValidDataLength 8
221 * 0x10 Reserved3 4
222 * 0x14 FirstCluster 4
223 * 0x18 DataLength 8
224 * ─────────────────────────────────
225 * 32
226 */
227typedef struct EXFAT_PACKED {
228 uint8_t entry_type; /* 0x00 0xC0 */
229 uint8_t flags; /* 0x01 GeneralSecondaryFlags */
230 uint8_t reserved1; /* 0x02 */
231 uint8_t name_length; /* 0x03 total filename chars */
232 uint16_t name_hash; /* 0x04 */
233 uint16_t reserved2; /* 0x06 */
234 uint64_t valid_data_length; /* 0x08 */
235 uint32_t reserved3; /* 0x10 */
236 uint32_t first_cluster; /* 0x14 */
237 uint64_t data_length; /* 0x18 */
238} exfat_stream_ext_t;
239 
240typedef char exfat_stream_ext_size_check[
241 (sizeof(exfat_stream_ext_t) == 32) ? 1 : -1];
242 
243/* ── Filename Entry (0xC1, 32 bytes) ────────────────────────────────────── */
244/*
245 * FIXED: removed name_length + name_hash fields that were placed beyond the
246 * 32-byte boundary, causing out-of-bounds reads in the original code.
247 * NameLength lives in the stream extension; each filename entry carries
248 * exactly 15 UTF-16LE code units.
249 *
250 * 0x00 EntryType 1
251 * 0x01 GeneralSecondaryFlags 1
252 * 0x02 FileName[15] (UTF-16LE) 30
253 * ──────────────────────────────────
254 * 32
255 */
256typedef struct EXFAT_PACKED {
257 uint8_t entry_type; /* 0x00 0xC1 */
258 uint8_t flags; /* 0x01 GeneralSecondaryFlags */
259 uint16_t file_name[15]; /* 0x02 up to 15 UTF-16LE chars */
260} exfat_filename_entry_t;
261 
262typedef char exfat_filename_entry_size_check[
263 (sizeof(exfat_filename_entry_t) == 32) ? 1 : -1];
264 
265/* ── Main context ───────────────────────────────────────────────────────── */
266 
267typedef struct {
268 FILE *image_file; /* open for reading; owned by context */
269 exfat_boot_sector_t boot_sector;
270 uint32_t bytes_per_sector;
271 uint32_t sectors_per_cluster;
272 uint32_t bytes_per_cluster;
273 uint64_t cluster_heap_offset_bytes; /* absolute byte offset in image */
274 uint32_t *fat_table;
275 size_t fat_entries;
276} exfat_context_t;
277 
278/* ── Parsed file/directory descriptor ──────────────────────────────────── */
279 
280typedef struct {
281 char filename[EXFAT_MAX_FILENAME_LEN + 1];
282 uint16_t attributes;
283 uint32_t first_cluster;
284 uint64_t data_length;
285 uint32_t create_time;
286 uint32_t last_modified_time;
287 uint32_t last_accessed_time;
288 int is_directory;
289 int no_fat_chain; /* 1 = contiguous clusters, FAT not needed */
290} exfat_file_info_t;
291 
292/* ── API ────────────────────────────────────────────────────────────────── */
293 
294/* Context lifecycle */
295int exfat_init(exfat_context_t *ctx, const char *image_path);
296void exfat_cleanup(exfat_context_t *ctx);
297 
298/* Boot sector */
299int exfat_read_boot_sector(exfat_context_t *ctx);
300int exfat_validate_boot_sector(const exfat_boot_sector_t *boot);
301void exfat_print_boot_sector(const exfat_boot_sector_t *boot);
302 
303/* FAT */
304int exfat_read_fat(exfat_context_t *ctx);
305uint32_t exfat_get_next_cluster(const exfat_context_t *ctx, uint32_t cluster);
306void exfat_free_fat(exfat_context_t *ctx);
307 
308/* Cluster I/O */
309uint64_t exfat_get_cluster_offset(const exfat_context_t *ctx, uint32_t cluster);
310ssize_t exfat_read_cluster(exfat_context_t *ctx, uint32_t cluster,
311 uint8_t *buffer, size_t size);
312 
313/* Directory */
314int exfat_read_directory(exfat_context_t *ctx, uint32_t first_cluster,
315 exfat_file_info_t *entries, int max_entries);
316int exfat_parse_file_entry(const uint8_t *data, size_t offset, size_t data_len,
317 exfat_file_info_t *info);
318int exfat_utf16_to_utf8(const uint16_t *utf16, int nchars,
319 char *utf8, size_t utf8_size);
320 
321/* File extraction */
322int exfat_extract_file(exfat_context_t *ctx, const exfat_file_info_t *info,
323 const char *output_path);
324int exfat_extract_file_fd(exfat_context_t *ctx, const exfat_file_info_t *info,
325 int output_fd);
326ssize_t exfat_extract_to_buffer(exfat_context_t *ctx, const exfat_file_info_t *info,
327 uint8_t *buf, size_t buf_size);
328 
329/* Utilities */
330void exfat_print_file_info(const exfat_file_info_t *info);
331time_t exfat_dos_time_to_unix(uint32_t dos_time);
332void exfat_format_size(uint64_t size, char *buffer, size_t buf_size);
333 
334/* ── Inline helpers ─────────────────────────────────────────────────────── */
335 
336static inline int exfat_is_valid_cluster(uint32_t c) {
337 return c >= 2U && c < EXFAT_FAT_BAD_CLUSTER;
338}
339static inline int exfat_is_end_of_chain(uint32_t c) {
340 return c >= EXFAT_FAT_BAD_CLUSTER;
341}
342static inline int exfat_is_directory(uint16_t attr) {
343 return (attr & EXFAT_ATTR_DIRECTORY) != 0;
344}
345static inline uint16_t exfat_read_le16(const uint8_t *p) {
346 return (uint16_t)((unsigned)p[0] | ((unsigned)p[1] << 8));
347}
348static inline uint32_t exfat_read_le32(const uint8_t *p) {
349 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
350 ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
351}
352static inline uint64_t exfat_read_le64(const uint8_t *p) {
353 return (uint64_t)exfat_read_le32(p) |
354 ((uint64_t)exfat_read_le32(p + 4) << 32);
355}
356 
357#ifdef __cplusplus
358}
359#endif
360 
361#if defined(_MSC_VER)
362# pragma pack(pop)
363#endif
364#undef EXFAT_PACKED
365 
366#endif /* EXFAT_UNPACKER_H */
367