am-fs-ext4 0.5.1

Pure-Rust ext4 filesystem driver. Exposes a C ABI (fs_ext4_*) suitable for FFI from C/C++/Go/etc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
/*
 * fs_ext4.h — C ABI for the ext4rs pure-Rust ext4 driver.
 *
 * Link against libfs_ext4.a and #include this header. UTF-8 paths,
 * NULL / -1 / 0 failure sentinels with thread-local error details
 * available via fs_ext4_last_error() / fs_ext4_last_errno().
 *
 * MIT License — see LICENSE
 */

#ifndef FS_EXT4_H
#define FS_EXT4_H

#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <sys/types.h>   /* mode_t, uid_t, gid_t */

#ifdef __cplusplus
extern "C" {
#endif

/* Opaque handle to a mounted ext4 filesystem */
typedef struct fs_ext4_fs fs_ext4_fs_t;

/* File type enumeration (matches ext4 dir entry types) */
typedef enum {
    FS_EXT4_FT_UNKNOWN  = 0,
    FS_EXT4_FT_REG_FILE = 1,
    FS_EXT4_FT_DIR      = 2,
    FS_EXT4_FT_CHRDEV   = 3,
    FS_EXT4_FT_BLKDEV   = 4,
    FS_EXT4_FT_FIFO     = 5,
    FS_EXT4_FT_SOCK     = 6,
    FS_EXT4_FT_SYMLINK  = 7,
} fs_ext4_file_type_t;

/* File/directory attributes.
 *
 * nsec fields are sub-second nanoseconds (0..999_999_999).  They are zero
 * when the on-disk inode's i_extra_isize is too small to hold the extra
 * timestamp words (ext2/ext3 inodes, or very old ext4 images).
 *
 * inode_flags mirrors the on-disk e2 flags word (FS_IOC_GETFLAGS convention):
 *   0x00000010  EXT4_IMMUTABLE_FL
 *   0x00000020  EXT4_APPEND_FL
 *   0x00000040  EXT4_NODUMP_FL
 *   0x00010000  EXT4_ENCRYPT_FL
 *   0x00020000  EXT4_CASEFOLD_FL
 *   …(full list in <linux/fs.h>)
 */
typedef struct {
    uint32_t inode;
    uint16_t mode;          /* POSIX mode bits */
    uint32_t uid;
    uint32_t gid;
    uint64_t size;
    /* Seconds since the Unix epoch, signed and 64-bit.
     *
     * Widened from uint32_t in 0.6.0. ext4's on-disk base field is a
     * SIGNED 32-bit value which the matching *_extra field extends by
     * two further bits, so the real range is roughly 1901..2446. A
     * uint32_t truncated everything past 2038 and turned every
     * pre-1970 date into a far-future one.
     *
     * THIS CHANGES THE STRUCT LAYOUT. Recompile any consumer. */
    int64_t  atime;
    int64_t  mtime;
    int64_t  ctime;
    int64_t  crtime;        /* Creation time (ext4 extra) */
    uint16_t link_count;
    fs_ext4_file_type_t file_type;
    /* Sub-second timestamp precision (added in v0.3) */
    uint32_t atime_nsec;
    uint32_t mtime_nsec;
    uint32_t ctime_nsec;
    uint32_t crtime_nsec;
    /* Inode flags (e2_flags / FS_IOC_GETFLAGS, added in v0.3) */
    uint32_t inode_flags;
    /* i_generation — monotone counter for NFS stale-handle detection */
    uint32_t generation;
    /* i_blocks in 512-byte units — matches st_blocks from stat(2) */
    uint64_t blocks_512;
} fs_ext4_attr_t;

/* Directory entry (returned during iteration) */
typedef struct {
    uint32_t inode;
    uint8_t  file_type;     /* fs_ext4_file_type_t */
    uint8_t  name_len;
    char     name[256];     /* null-terminated */
} fs_ext4_dirent_t;

/* Volume information */
/*
 * Snapshot of everything in the on-disk superblock that a UI / CLI
 * consumer might want to surface. Layout is part of the FFI ABI; new
 * fields land at the end so existing zero-init memcpy callers stay
 * valid. Field-by-field documentation lives in src/capi.rs alongside
 * the rust definition.
 */
typedef struct {
    /* ----- Identity ----- */
    char     volume_name[16];     /* NUL-terminated, <= 16 bytes */
    uint8_t  uuid[16];            /* raw 16-byte UUID */
    char     last_mounted[64];    /* NUL-terminated last-mount path, or "" */

    /* ----- Sizing ----- */
    uint32_t block_size;
    uint64_t total_blocks;
    uint64_t free_blocks;
    uint64_t reserved_blocks;     /* root-reserve blocks */
    uint32_t total_inodes;
    uint32_t free_inodes;
    uint16_t inode_size;
    uint32_t first_inode;         /* first non-reserved inode (s_first_ino) */
    uint32_t blocks_per_group;
    uint32_t inodes_per_group;

    /* ----- Provenance + capabilities ----- */
    uint32_t creator_os;          /* 0=Linux 1=Hurd 2=Masix 3=FreeBSD 4=Lites */
    uint32_t rev_level;
    uint16_t minor_rev_level;
    uint32_t feature_compat;
    uint32_t feature_incompat;
    uint32_t feature_ro_compat;
    uint16_t desc_size;           /* block-group descriptor size: 32 or 64 */
    uint8_t  default_hash_version;

    /* ----- Lifecycle / health ----- */
    uint16_t state;               /* s_state bit field */
    uint16_t errors_behavior;     /* s_errors: 1=continue 2=remount-ro 3=panic */
    uint32_t last_mount_time;     /* unix epoch seconds */
    uint32_t last_write_time;     /* unix epoch seconds */
    uint32_t last_check_time;     /* unix epoch seconds */
    uint32_t check_interval;      /* seconds between forced fsck; 0 = disabled */
    uint16_t mount_count;
    uint16_t max_mount_count;     /* 0 = unlimited */
    uint16_t def_resuid;
    uint16_t def_resgid;

    /*
     * 1 if the filesystem was NOT cleanly unmounted last time it was used
     * (captured from the on-disk s_state field at mount time, before any
     * journal replay the driver may perform). 0 if clean. Consumers should
     * surface a dirty value to the user and run fsck / journal replay
     * before permitting writes. Derived from `state` for caller convenience.
     */
    uint8_t  mounted_dirty;
} fs_ext4_volume_info_t;

/* ---- Block device callback interface ---- */

/*
 * Callback for reading blocks from the device.
 * Must read exactly `length` bytes at `offset` into `buf`.
 * Returns 0 on success, non-zero on error.
 * `context` is the opaque pointer passed to fs_ext4_mount_with_callbacks.
 */
typedef int (*fs_ext4_read_fn)(void *context, void *buf,
                                   uint64_t offset, uint64_t length);

/*
 * Callback for writing blocks to the device.
 * Must write exactly `length` bytes from `buf` starting at `offset`.
 * Returns 0 on success, non-zero on error.
 *
 * Optional — set to NULL when mounting read-only via
 * fs_ext4_mount_with_callbacks. Required (must be non-NULL) when mounting
 * read-write via fs_ext4_mount_rw_with_callbacks.
 */
typedef int (*fs_ext4_write_fn)(void *context, const void *buf,
                                    uint64_t offset, uint64_t length);

/*
 * Optional flush/fsync callback. Invoked when the driver wants pending
 * writes pushed to stable storage. May be NULL — the driver then treats
 * flush as a no-op (this is what FSKit's FSBlockDeviceResource already
 * does, as it batches synchronisation at a higher layer).
 */
typedef int (*fs_ext4_flush_fn)(void *context);

/*
 * Block device parameters for callback-based mounting.
 *
 * NOTE: `write` and `flush` were appended at the tail of the struct in
 * v0.1.3 to keep backward-compatible binary layout with v0.1.2 consumers.
 * Existing read-only callers that memset/zero-init their config are
 * unaffected — fs_ext4_mount_with_callbacks ignores both new fields and
 * always mounts read-only, regardless of what's in `write` / `flush`.
 */
typedef struct {
    fs_ext4_read_fn read;
    void   *context;     /* Passed to callbacks (e.g. FSBlockDeviceResource pointer) */
    uint64_t size_bytes; /* Total device/partition size */
    uint32_t block_size; /* Physical block size (e.g. 512) */
    fs_ext4_write_fn write; /* NEW in v0.1.3; NULL if read-only */
    fs_ext4_flush_fn flush; /* NEW in v0.1.3; NULL = flush is a no-op */
} fs_ext4_blockdev_cfg_t;

/* ---- Lifecycle ---- */

/*
 * Mount an ext4 filesystem from the given device/image path.
 * Uses direct POSIX I/O. Returns NULL on failure. Read-only.
 */
fs_ext4_fs_t *fs_ext4_mount(const char *device_path);

/*
 * Mount an ext4 filesystem using callback-based I/O.
 * Use this from sandboxed environments (e.g. FSKit extensions)
 * where direct device access is not available.
 * Returns NULL on failure. Read-only — `cfg->write` / `cfg->flush` are
 * ignored even if non-NULL. Use fs_ext4_mount_rw_with_callbacks for
 * read-write mounts.
 */
fs_ext4_fs_t *fs_ext4_mount_with_callbacks(
    const fs_ext4_blockdev_cfg_t *cfg);

/*
 * Mount via an FsCoreDevice handle from a sister crate
 * (`qcow2_open` from am-img-qcow2, `partitions_open_slice` from
 * am-partitions, `fs_core_file_open` from am-fs-core).
 *
 * Single entry point — RO vs RW is decided by the inner device's
 * `is_writable()`, so callers don't need a `_rw` variant.
 *
 * The handle's reference count is incremented internally; the caller
 * still owns their *FsCoreDevice and frees it via
 * `fs_core_device_close`. Closing the resulting fs_ext4_fs_t via
 * `fs_ext4_umount` drops the mount's own reference.
 *
 * Forward declared FsCoreDevice — full definition in `fs_core.h`.
 *
 * Returns NULL on failure; use fs_ext4_last_error() / fs_ext4_last_errno()
 * for detail.
 */
struct FsCoreDevice;
fs_ext4_fs_t *fs_ext4_mount_with_fs_core_device(struct FsCoreDevice *handle);

/*
 * Same as fs_ext4_mount_with_fs_core_device but defers journal replay.
 * Use this when the embedding context cannot tolerate replay running
 * synchronously inside the mount call — typically because the device
 * handle isn't fully writable until the mount call has returned.
 *
 * After mount, call `fs_ext4_replay_journal_if_dirty(fs)` once the
 * underlying write path is ready. Until journal replay runs, the
 * mounted state may reflect a partially-applied journal — readers
 * see what's on disk, not the post-replay view. Writes through the
 * crate will fail until replay completes.
 *
 * Same handle-ownership semantics as fs_ext4_mount_with_fs_core_device:
 * the handle's reference count is incremented internally; the caller
 * still owns their *FsCoreDevice and frees it via
 * `fs_core_device_close`. Closing the resulting fs_ext4_fs_t via
 * `fs_ext4_umount` drops the mount's own reference.
 *
 * Returns NULL on failure; use fs_ext4_last_error() / fs_ext4_last_errno()
 * for detail.
 */
fs_ext4_fs_t *fs_ext4_mount_with_fs_core_device_lazy(struct FsCoreDevice *handle);

/*
 * Mount an ext4 filesystem read-write using callback-based I/O.
 * Companion to fs_ext4_mount_rw — same behaviour (replays a dirty journal
 * before returning), but the device is reached through caller-supplied
 * read/write callbacks instead of a path. Suitable for FSKit extensions
 * that own an FSBlockDeviceResource and cannot open /dev/diskN directly.
 *
 * Both `cfg->read` AND `cfg->write` must be non-NULL — otherwise this
 * returns NULL with errno set to EINVAL. `cfg->flush` is optional; pass
 * NULL to make synchronize() a no-op (the caller's host I/O layer is
 * expected to handle stable-storage barriers in that case).
 *
 * Returns NULL on failure; use fs_ext4_last_error() / fs_ext4_last_errno()
 * to inspect the cause.
 */
fs_ext4_fs_t *fs_ext4_mount_rw_with_callbacks(
    const fs_ext4_blockdev_cfg_t *cfg);

/*
 * Mount RW via callbacks WITHOUT performing journal replay automatically.
 * Same semantics as `fs_ext4_mount_rw_with_callbacks` except a dirty
 * journal is recorded but NOT replayed during this call. Use this when
 * the consumer is in a context where its write callback can't service
 * writes yet (e.g. inside FSKit's loadResource, before the kernel opens
 * the writable FD on FSBlockDeviceResource).
 *
 * After mount, call `fs_ext4_replay_journal_if_dirty(fs)` once the
 * consumer's write path is ready. Until journal replay runs, the
 * mounted state may reflect a partially-applied journal — readers
 * see what's on disk, not the post-replay view. Writes through the
 * crate will fail until replay completes.
 *
 * Returns NULL on failure (cfg->read or cfg->write NULL, etc).
 */
fs_ext4_fs_t *fs_ext4_mount_rw_with_callbacks_lazy(
    const fs_ext4_blockdev_cfg_t *cfg);

/*
 * If the volume's journal is dirty, replay it now. Idempotent — safe
 * to call when clean (returns 0, no writes). Returns 0 on success or
 * already-clean, -1 on failure (call fs_ext4_last_error / _last_errno
 * for details). The handle must have been mounted via the `_lazy`
 * variant above; calling on a handle from the eager mount is a no-op
 * and returns 0.
 */
int fs_ext4_replay_journal_if_dirty(fs_ext4_fs_t *fs);

/*
 * Unmount and free all resources.
 */
void fs_ext4_umount(fs_ext4_fs_t *fs);

/* ---- Volume info ---- */

/*
 * Get volume information (name, sizes, free space).
 * Returns 0 on success.
 */
int fs_ext4_get_volume_info(fs_ext4_fs_t *fs,
                                fs_ext4_volume_info_t *info);

/* ---- File attributes ---- */

/*
 * Get attributes for a path (relative to mount root).
 * path should start with "/" e.g. "/etc/passwd"
 * Returns 0 on success.
 */
int fs_ext4_stat(fs_ext4_fs_t *fs, const char *path,
                     fs_ext4_attr_t *attr);

/* ---- Directory listing ---- */

/*
 * Directory iterator — opaque handle.
 */
typedef struct fs_ext4_dir_iter fs_ext4_dir_iter_t;

/*
 * Open a directory for iteration.
 * Returns NULL on failure.
 */
fs_ext4_dir_iter_t *fs_ext4_dir_open(fs_ext4_fs_t *fs,
                                              const char *path);

/*
 * Get the next directory entry.
 * Returns pointer to internal dirent (valid until next call or close).
 * Returns NULL when no more entries.
 */
const fs_ext4_dirent_t *fs_ext4_dir_next(fs_ext4_dir_iter_t *iter);

/*
 * Close directory iterator.
 */
void fs_ext4_dir_close(fs_ext4_dir_iter_t *iter);

/* ---- File reading ---- */

/*
 * Read file contents.
 * Returns bytes read, or -1 on error.
 */
int64_t fs_ext4_read_file(fs_ext4_fs_t *fs, const char *path,
                              void *buf, uint64_t offset, uint64_t length);

/* ---- Symlink ---- */

/*
 * Read symlink target.
 * Writes null-terminated target into buf (max bufsize bytes).
 * Returns 0 on success.
 */
int fs_ext4_readlink(fs_ext4_fs_t *fs, const char *path,
                         char *buf, size_t bufsize);

/* ---- Extended attributes ---- */

/*
 * List extended attribute names for a path.
 *
 * Writes NUL-separated fully-qualified names (e.g. "user.color\0user.tag\0")
 * into buf. If buf is NULL or bufsize is 0, no bytes are written but the
 * return value still reports the required total size — use this to probe.
 *
 * Returns: total bytes of output (names + NUL terminators) on success,
 *          -1 on error. If bufsize is less than the required size, writes
 *          as much as fits and still returns the required size.
 */
int64_t fs_ext4_listxattr(fs_ext4_fs_t *fs, const char *path,
                              char *buf, size_t bufsize);

/*
 * Get a single extended attribute value by fully-qualified name
 * (e.g. "user.color", "system.posix_acl_access").
 *
 * Writes raw value bytes (no NUL-terminator) into buf. If buf is NULL or
 * bufsize is 0, returns the value size without writing — use this to probe.
 *
 * Returns: value size in bytes on success,
 *          -1 if the name is not present or on error. If bufsize is less
 *          than the value size, writes as much as fits and still returns
 *          the value size.
 */
int64_t fs_ext4_getxattr(fs_ext4_fs_t *fs, const char *path,
                             const char *name, void *buf, size_t bufsize);

/* ---- Error reporting ---- */

/*
 * Get last error message (thread-local).
 * Returns pointer to static/thread-local string.
 */
const char *fs_ext4_last_error(void);

/*
 * Get POSIX errno for the last failed FFI call (thread-local).
 * Returns 0 if the last call succeeded (or no call has been made yet).
 * Codes: ENOENT, EIO, ENOTDIR, EINVAL, ENOTSUP — or any errno surfaced
 * by the underlying I/O layer (e.g. EACCES from the block device).
 * Use this alongside fs_ext4_last_error() to produce an NSError
 * with the correct POSIXErrorDomain code for FSKit.
 */
int fs_ext4_last_errno(void);

/*
 * ----- Write path (Phase 4, in progress) ------------------------------
 *
 * These exports require a read-write mount. Use fs_ext4_mount_rw()
 * for file-backed images; the existing callback mount is read-only.
 * On failure, -1 is returned and fs_ext4_last_error / _last_errno
 * describe the cause.
 */

/* Mount an ext4 filesystem read-write. Returns NULL on failure. A dirty
 * JBD2 journal is replayed before returning so callers see a consistent
 * on-disk state. */
fs_ext4_fs_t *fs_ext4_mount_rw(const char *device_path);

/* Shrink a regular file to `new_size` bytes. Frees the tail extents and
 * updates the inode size + blocks counter. Not yet journaled — safe only
 * on scratch images until the transaction wrapping lands. */
int fs_ext4_truncate(fs_ext4_fs_t *fs, const char *path,
                         uint64_t new_size);

/* Unlink a non-directory file at `path`. Decrements i_links_count; when
 * the count reaches zero the inode's extents are freed, its bitmap bit
 * cleared, and its body zeroed (with i_dtime = now). Refuses directories.
 * Returns 0 on success, -1 on failure. Not yet journaled. */
int fs_ext4_unlink(fs_ext4_fs_t *fs, const char *path);

/* Create a new empty regular file at `path` with the given permission
 * bits (e.g. 0644). Parent must exist and be a directory; the path must
 * not already exist. Returns the allocated inode number on success
 * (> 0), or 0 on failure. Not yet journaled. */
uint32_t fs_ext4_create(fs_ext4_fs_t *fs, const char *path,
                            uint16_t mode);

/* Replace the contents of an existing regular file with `len` bytes from
 * `data`. Frees any previous extents, allocates one contiguous run for
 * the new data, updates size + mtime + ctime. Returns the new size on
 * success, or -1 on failure. Not yet journaled; appends / partial writes
 * are follow-up work. */
int64_t fs_ext4_write_file(fs_ext4_fs_t *fs, const char *path,
                               const void *data, uint64_t len);

/* Positional write — splice `len` bytes from `data` into `path` at byte
 * `offset`. Allocates new physical blocks for any logical blocks not yet
 * mapped (sparse holes / past EOF); existing mapped blocks are read-
 * modify-written so untouched bytes stay intact. The file must already
 * exist (use `fs_ext4_create` first).
 *
 * This is the streaming-write primitive — cost is O(len), not
 * O(filesize). FUSE / WinFsp / FSKit cache-manager dispatches should
 * call this directly instead of merging into a whole-file replace.
 *
 * Returns the new file size on success (>= offset+len), or -1 on
 * failure. Hard cap on `len`: 1 GiB per call. */
int64_t fs_ext4_pwrite(fs_ext4_fs_t *fs, const char *path,
                           const void *data, uint64_t len, uint64_t offset);

/* Rename / move `src` to `dst` within this filesystem. Supports files
 * and directories; cross-parent dir moves fix `..` + parent link counts.
 * Dest must not already exist — for atomic overwrite use
 * `fs_ext4_rename2` with the `FS_EXT4_RENAME_REPLACE` flag. Returns 0
 * on success, -1 on failure. Not yet journaled. */
int fs_ext4_rename(fs_ext4_fs_t *fs, const char *src,
                       const char *dst);

/* Flag bits accepted by `fs_ext4_rename2`. `FS_EXT4_RENAME_REPLACE`
 * enables atomic overwrite of an existing destination, matching POSIX
 * `rename(2)` semantics — required so Windows Explorer "Save As" /
 * drag-drop-onto-existing flows succeed instead of silently failing.
 * Unknown flag bits are rejected with EINVAL so future flag additions
 * stay forward-compatible. */
#define FS_EXT4_RENAME_REPLACE 0x01

/* Rename / move `src` to `dst` within this filesystem with explicit
 * flags. With `FS_EXT4_RENAME_REPLACE` an existing destination is
 * atomically replaced: file→file overwrites and frees the old data,
 * empty-dir → empty-dir overwrites the dropped subdir, crossing the
 * file/directory boundary returns EISDIR / ENOTDIR, and a non-empty
 * destination directory returns ENOTEMPTY. Without the flag, identical
 * to `fs_ext4_rename`. Returns 0 on success, -1 on failure. */
int fs_ext4_rename2(fs_ext4_fs_t *fs, const char *src,
                        const char *dst, int flags);

/* Create a hard link at `dst` pointing to the same inode as `src`.
 * Forbidden on directories. Dest must not already exist. Bumps the
 * shared inode's i_links_count by 1. Returns 0 on success, -1 on
 * failure. Not yet journaled. */
int fs_ext4_link(fs_ext4_fs_t *fs, const char *src,
                     const char *dst);

/* Create a subdirectory at `path` with POSIX permission bits `mode`
 * (typically 0755; low 12 bits used). Parent must exist and be a
 * directory; the path must not already exist. Seeds the new dir with
 * `.` and `..` entries and bumps the parent's i_links_count.
 * Returns the new directory's inode number on success (> 0), or 0 on
 * failure. Not yet journaled. */
uint32_t fs_ext4_mkdir(fs_ext4_fs_t *fs, const char *path,
                           uint16_t mode);

/* Remove an empty directory at `path`. Fails if the target contains
 * entries other than `.` and `..`. Frees its data blocks and inode,
 * removes the entry from the parent, and decrements the parent's
 * i_links_count. Returns 0 on success, -1 on failure. Not yet
 * journaled. */
int fs_ext4_rmdir(fs_ext4_fs_t *fs, const char *path);

/* Change the permission bits on `path`. Only the low 12 bits of `mode`
 * (suid/sgid/sticky + rwx/rwx/rwx) are applied; file-type bits are
 * preserved. Bumps i_ctime. Returns 0 on success, -1 on failure. */
int fs_ext4_chmod(fs_ext4_fs_t *fs, const char *path, uint16_t mode);

/* Change the owner of `path` to (`uid`, `gid`). Passing UINT32_MAX
 * (0xFFFFFFFF) for either parameter leaves that value unchanged
 * (matches Linux chown(2) semantics). Bumps i_ctime. Returns 0 on
 * success, -1 on failure. */
int fs_ext4_chown(fs_ext4_fs_t *fs, const char *path,
                  uint32_t uid, uint32_t gid);

/* Create a special file (FIFO, socket, char device, block device).
 * `mode` must include the file-type bits (S_IFIFO=0x1000, S_IFSOCK=0xC000,
 * S_IFCHR=0x2000, S_IFBLK=0x6000) plus permission bits.
 * `major`/`minor` are device numbers for char/block devices; pass 0
 * for FIFOs and sockets. Returns the new inode number on success, 0 on
 * failure. */
uint32_t fs_ext4_mknod(fs_ext4_fs_t *fs, const char *path,
                       uint16_t mode, uint32_t major, uint32_t minor);

/* Set the i_flags word (FS_IOC_SETFLAGS) on `path`. `flags` is the
 * full new flags value; common flags:
 *   0x00000010  EXT4_IMMUTABLE_FL
 *   0x00000020  EXT4_APPEND_FL
 *   0x00000040  EXT4_NODUMP_FL
 *   0x00000200  EXT4_NOATIME_FL
 * Bumps i_ctime. Returns 0 on success, -1 on failure. */
int fs_ext4_set_flags(fs_ext4_fs_t *fs, const char *path, uint32_t flags);

/* Create a symbolic link at `linkpath` whose target is `target`.
 * POSIX symlink(target, linkpath) semantics. v1: fast-symlink only
 * (target ≤ 60 bytes); longer targets return 0 with EINVAL. Returns
 * the new symlink's inode number on success, 0 on failure. */
uint32_t fs_ext4_symlink(fs_ext4_fs_t *fs, const char *target,
                         const char *linkpath);

/* Set (create or replace) the extended attribute `name` on `path` with
 * `value_len` bytes from `value`. `name` must be fully-qualified
 * (e.g. "user.com.apple.FinderInfo"). v1: in-inode xattrs only;
 * ENOSPC if the in-inode region is too small; external-block spill
 * not implemented. Returns 0 on success, -1 on failure. */
int fs_ext4_setxattr(fs_ext4_fs_t *fs, const char *path,
                     const char *name, const void *value,
                     size_t value_len);

/* Remove the extended attribute `name` from the inode at `path`.
 * `name` must be fully-qualified (carry a known namespace prefix like
 * "user." or "security."). v1: in-inode xattrs only; external-block
 * removal surfaces EINVAL until the slow path lands. ENOENT when the
 * name isn't present. Returns 0 on success, -1 on failure. */
int fs_ext4_removexattr(fs_ext4_fs_t *fs, const char *path,
                        const char *name);

/* Leave a timestamp unchanged: POSIX spells this UTIME_OMIT in the
 * nanoseconds field; fs_ext4_utimens takes it in the seconds field.
 * Not UINT32_MAX any more -- seconds are signed and 64-bit, which makes
 * that value an ordinary date in 2106 rather than a spare bit pattern.
 *
 * A `static const` and not a `#define` so that Swift sees it. Swift's
 * C importer takes simple literal macros only; `#define FS_EXT4_TIME_OMIT
 * INT64_MIN` expands to an expression it declines to import, and the
 * symbol is then simply absent on the Swift side. */
static const int64_t FS_EXT4_TIME_OMIT = INT64_MIN;

/* Set the access + modification times on `path`. Each `*_sec` is a
 * POSIX seconds-since-epoch value; passing FS_EXT4_TIME_OMIT leaves that
 * pair unchanged (so `atime_sec == FS_EXT4_TIME_OMIT` touches only
 * mtime, etc). `*_nsec` is sub-second nanoseconds, only written when the
 * inode's i_extra_isize region can hold them. Bumps i_ctime.
 *
 * The seconds are int64_t (widened from uint32_t in 0.5.0) because ext4
 * stores a SIGNED 32-bit base extended by two epoch bits in the matching
 * `*_extra` field -- roughly 1901..2446. The old unsigned type could
 * express neither end: a pre-1970 date was unrepresentable, and a
 * post-2038 date was written with the epoch bits left zero and read back
 * 136 years early.
 *
 * Returns 0 on success, -1 on failure. Fails with EINVAL for a time
 * outside that range, or for one needing the epoch bits on an inode too
 * small to hold an `*_extra` field (128-byte inodes). */
int fs_ext4_utimens(fs_ext4_fs_t *fs, const char *path,
                    int64_t atime_sec, uint32_t atime_nsec,
                    int64_t mtime_sec, uint32_t mtime_nsec);

/* ---- Volume creation (mkfs) ---- */

/*
 * Format a block device as a fresh ext4 filesystem. The device is reached
 * through the same callback shape used for mounting — `cfg->read`, `cfg->write`
 * and `cfg->size_bytes` must all be set; `cfg->flush` is optional. Works for
 * both `/dev/diskN` (real disk) and disk-image files when the caller wires
 * file-backed callbacks.
 *
 * `label` is an optional NUL-terminated UTF-8 volume name (≤ 16 bytes; longer
 * names are truncated). Pass NULL to leave it blank.
 * `uuid` is either NULL (driver generates a v4 UUID from /dev/urandom) or a
 * pointer to exactly 16 raw bytes the caller has chosen.
 *
 * On-disk layout (v1, single block-group, ≤ 32k blocks): boot sector zero,
 * primary superblock, BGD table, block bitmap, inode bitmap, inode table,
 * root directory data block. Features enabled: FILETYPE, EXTENTS, 64BIT,
 * METADATA_CSUM. No journal — the FS mounts cleanly without one.
 *
 * Returns 0 on success or -errno on failure. Use fs_ext4_last_error /
 * fs_ext4_last_errno for the cause.
 */
int fs_ext4_mkfs(const fs_ext4_blockdev_cfg_t *cfg,
                 const char *label,
                 const uint8_t *uuid);

/*
 * ----- Read-only fsck (filesystem audit) ------------------------------
 *
 * Walks the directory tree, counts directory-entry references to each
 * inode, and reports inconsistencies via callbacks. Never writes — the
 * `read_only` field of `fs_ext4_fsck_options_t` MUST be 1 in this MVP.
 * Repair is explicit future work; a future ABI addition will cover it.
 *
 * Findings are streamed through `on_finding` rather than collected
 * into a returned list, so the host UI can render progress live for
 * very large volumes without buffering the full anomaly set.
 */

typedef enum {
    FS_EXT4_FSCK_PHASE_SUPERBLOCK = 0,
    FS_EXT4_FSCK_PHASE_JOURNAL    = 1,
    FS_EXT4_FSCK_PHASE_DIRECTORY  = 2,
    FS_EXT4_FSCK_PHASE_INODES     = 3,
    FS_EXT4_FSCK_PHASE_FINALIZE   = 4,
} fs_ext4_fsck_phase_t;

/*
 * Progress callback. `phase_name` points to a short ASCII string
 * ("superblock", "journal", "directory", "inodes", "finalize") and is
 * valid only for the duration of the call. `done` and `total` are
 * monotonic within a single phase; `total` may grow during DIRECTORY
 * (queued work is best-estimate). Either or both may be 0 in
 * pathological cases.
 */
typedef void (*fs_ext4_fsck_progress_fn)(void *context,
    fs_ext4_fsck_phase_t phase, const char *phase_name,
    uint64_t done, uint64_t total);

/*
 * Per-finding callback. `kind` is one of: "link_count_low",
 * "link_count_high", "dangling_entry", "wrong_dotdot", "bogus_entry",
 * "duplicate_dir_inode", "block_group_free_count_drift",
 * "superblock_free_count_drift". `inode` is the most relevant inode
 * (affected inode for link-count cases; child for dangling_entry and
 * bogus_entry; directory for wrong_dotdot; duplicated dir inode for
 * duplicate_dir_inode; group_index for block_group_free_count_drift;
 * 0 for superblock_free_count_drift — i.e. for the drift kinds the
 * field is overloaded and does not carry an inode number). `detail`
 * is a short, free-form ASCII blob like "stored=1 observed=2" or
 * "parent_ino=2" — for diagnostic display only, not meant to be
 * parsed. Both `kind` and `detail` are valid only for the duration of
 * the call.
 */
typedef void (*fs_ext4_fsck_finding_fn)(void *context,
    const char *kind, uint32_t inode, const char *detail);

typedef struct {
    uint8_t  read_only;            /* 1 = audit only (no writes); 0 = allow repair */
    uint8_t  replay_journal;       /* 1 = invoke replay_journal_if_dirty first */
    uint32_t max_dirs;             /* 0 = unbounded (use u32::MAX internally) */
    uint32_t max_entries_per_dir;  /* 0 = unbounded */
    fs_ext4_fsck_progress_fn on_progress; /* nullable */
    fs_ext4_fsck_finding_fn  on_finding;  /* nullable */
    void *context;
    /*
     * Repair-pass switch. When 1 (and `read_only` is 0) the audit
     * pass commits journaled fixes for the corruption classes it
     * knows how to handle today:
     *   - duplicate dir-entry → same directory inode
     *   - link-count drift (i_links_count != observed dirent count)
     *   - wrong_dotdot (.. claim disagrees with walker's parent)
     *   - bogus_entry (dirent file_type vs. inode mode mismatch)
     *   - dangling_entry (link-count rescue when child is readable)
     *   - block_group_free_count_drift / superblock_free_count_drift
     * Other anomalies are still detected and reported but not
     * modified. When 0 (or `read_only` == 1) the run is purely
     * diagnostic. Must be 0 or 1; other values are rejected as
     * EINVAL.
     */
    uint8_t  repair;
} fs_ext4_fsck_options_t;

typedef struct {
    uint64_t inodes_visited;
    uint64_t directories_scanned;
    uint64_t entries_scanned;
    /* Authoritative current anomaly count. After a repair pass this
     * is the post-repair RE-SCAN count, not pre-repair minus
     * repaired_count. */
    uint64_t anomalies_found;
    uint8_t  was_dirty;            /* 1 if SB.s_state showed dirty pre-run */
    uint8_t  dirty_cleared;        /* 1 if a repair commit cleared the dirty bit */
    /*
     * Number of anomalies the repair pass committed a fix for. 0 when
     * `repair` was off, when no repairable anomalies were found, or
     * when every found anomaly is in a class fsck doesn't repair yet.
     */
    uint64_t repaired_count;
    /*
     * Anomalies the audit found BEFORE any repair commits. Equal to
     * `anomalies_found` for non-repair runs. After a repair pass:
     * `initial_anomalies_count - repaired_count` is what we EXPECT
     * to remain; `anomalies_found` is what ACTUALLY remains.
     * Mismatches indicate repair-logic bugs.
     */
    uint64_t initial_anomalies_count;
} fs_ext4_fsck_report_t;

/*
 * Run a read-only fsck audit. Writes summary counters to *report and
 * delivers each anomaly through opts->on_finding as it is discovered.
 *
 * Returns 0 on success regardless of how many anomalies were found —
 * anomalies don't fail the call, they're surfaced via on_finding and
 * counted in report->anomalies_found. Returns -1 on hard failure
 * (null args, read_only != 1, journal replay error, I/O error during
 * walk); use fs_ext4_last_error / _last_errno for the cause.
 */
int fs_ext4_fsck_run(fs_ext4_fs_t *fs,
    const fs_ext4_fsck_options_t *opts,
    fs_ext4_fsck_report_t *report);

#ifdef __cplusplus
}
#endif

#endif /* FS_EXT4_H */