hadris-fat 2.0.0

Rust FAT12/FAT16/FAT32 filesystem library for disk images, embedded devices, and no-std
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
// mkfs_sectors.bin is a binary file generated using these commands:
// # Create the image (100MB)
// dd if=/dev/zero of=test.img bs=512 count=204800
// # Create the filesystem
// mkfs.fat -F 32 test.img
// # Copy first 2 sectors to the mkfs_sectors.bin file
// dd if=test.img of=mkfs_sectors.bin bs=512 count=2
const BOOT_SECTORS: &[u8] = include_bytes!("mkfs_sectors.bin");

use hadris_fat::{Error, FatVolume};
use std::io::Cursor;

/// Test that boot sector parsing works correctly with the mkfs_sectors.bin fixture
#[test]
fn test_parse_boot_sector() {
    // The mkfs_sectors.bin contains the first 2 sectors of a valid FAT32 image
    // (boot sector and FSInfo sector)
    let data = Cursor::new(BOOT_SECTORS.to_vec());

    // Try to open the filesystem - this tests our parsing code
    // The open may succeed or fail depending on how much data we need to read
    let result = FatVolume::open(data);

    // Whether it succeeds or fails, it should not panic
    // If it succeeds, great! If it fails, check that it's a reasonable error
    match result {
        Ok(_fs) => {
            // Successfully parsed the boot sector - that's fine
        }
        Err(Error::Io(_)) => {
            // I/O error (e.g., trying to read beyond buffer) is acceptable
        }
        Err(Error::InvalidFsInfoSignature { .. }) => {
            // FSInfo validation error is acceptable if data is truncated
        }
        Err(e) => {
            // Other errors should be investigated
            panic!("Unexpected error parsing boot sector: {e:?}");
        }
    }
}

/// Test that invalid boot signature is detected
#[test]
fn test_invalid_boot_signature() {
    // Preserve a valid BPB so this test isolates the signature check.
    let mut data = BOOT_SECTORS.to_vec();
    data[510] = 0x00; // Wrong signature (should be 0x55)
    data[511] = 0x00; // Wrong signature (should be 0xAA)

    let cursor = Cursor::new(data);
    let result = FatVolume::open(cursor);

    match result {
        Err(Error::InvalidBootSignature { found }) => {
            assert_eq!(found, 0x0000);
        }
        _ => panic!("Expected InvalidBootSignature error"),
    }
}

/// A corrupt BPB fat_count (not 1 or 2) must return an error, not trip the
/// `debug_assert!(count == 1 || count == 2)` inside the FAT constructors.
/// (fuzz regression: fat_table.rs debug_assert on untrusted input)
#[test]
fn test_invalid_fat_count_rejected() {
    for bad in [0u8, 3, 0xFF] {
        let mut data = BOOT_SECTORS.to_vec();
        data[16] = bad; // BPB_NumFATs
        match FatVolume::open(Cursor::new(data)) {
            Err(Error::CorruptFilesystem { .. }) => {}
            other => panic!("fat_count={bad} should be rejected, got {other:?}"),
        }
    }
}

/// Test that FAT12/16 is detected and parsed.
#[test]
fn test_fat12_16_detection() {
    // Create a minimal FAT16-like buffer
    // This won't have enough data for a fully valid filesystem, but should
    // be detected as FAT12/16 based on root_entry_count and sectors_per_fat_16
    let mut data = vec![0u8; 4096];

    // Set boot jump
    data[0] = 0xEB;
    data[1] = 0x58;
    data[2] = 0x90;

    // Set bytes per sector = 512
    data[11] = 0x00;
    data[12] = 0x02;

    // Set sectors per cluster = 1
    data[13] = 0x01;

    // Set reserved sectors = 1
    data[14] = 0x01;
    data[15] = 0x00;

    // Set FAT count = 2
    data[16] = 0x02;

    // Set root_entry_count = 512 (non-zero indicates FAT12/16)
    data[17] = 0x00; // Little-endian
    data[18] = 0x02; // 512 entries

    // Set total_sectors_16 = 0 (we'll use total_sectors_32)
    data[19] = 0x00;
    data[20] = 0x00;

    // Set media type
    data[21] = 0xF8;

    // Set sectors_per_fat_16 = 1 (non-zero indicates FAT12/16)
    data[22] = 0x01;
    data[23] = 0x00;

    // Set total_sectors_32 (at offset 32) = 2880 (small disk)
    data[32] = 0x40;
    data[33] = 0x0B;
    data[34] = 0x00;
    data[35] = 0x00;

    // Set boot signature at 510-511 (within first sector)
    data[510] = 0x55;
    data[511] = 0xAA;

    let cursor = Cursor::new(data);
    let result = FatVolume::open(cursor);

    // Now FAT12/16 should be detected, though the filesystem may not be fully valid
    // We just check that it doesn't return UnsupportedFatType
    match result {
        Ok(fs) => {
            // Check that it detected as FAT12 or FAT16
            use hadris_fat::FatType;
            assert!(matches!(fs.fat_type(), FatType::Fat12 | FatType::Fat16));
        }
        Err(Error::UnsupportedFatType(_)) => {
            panic!("FAT12/16 should now be supported");
        }
        Err(_) => {
            // Other errors are acceptable (e.g., I/O errors from incomplete data)
        }
    }
}

#[cfg(test)]
mod file_tests {
    use hadris_fat::file::ShortFileName;

    #[test]
    fn test_short_filename_valid() {
        // Valid 8.3 filename "TEST    TXT"
        let name = *b"TEST    TXT";
        let result = ShortFileName::new(name);
        assert!(result.is_ok());
        let sfn = result.unwrap();
        assert!(sfn.as_str().starts_with("TEST"));
    }

    #[test]
    fn test_short_filename_with_spaces() {
        // Filename with spaces is valid
        let name = *b"FILE    BIN";
        let result = ShortFileName::new(name);
        assert!(result.is_ok());
    }

    #[test]
    fn test_short_filename_invalid_lowercase() {
        // Lowercase letters are not valid in short filenames
        let name = *b"test    txt";
        let result = ShortFileName::new(name);
        // Note: Our current implementation allows lowercase, you may want to change this
        // For now this tests that the function doesn't panic
        let _ = result;
    }

    #[test]
    fn test_short_filename_special_chars() {
        // Test allowed special characters
        let result = ShortFileName::new([
            b'$', b'%', b'\'', b'-', b'_', b'@', b'~', b' ', b' ', b' ', b' ',
        ]);
        assert!(result.is_ok());
    }
}

#[cfg(feature = "lfn")]
#[cfg(test)]
mod lfn_tests {
    use hadris_fat::file::{LfnBuilder, LongFileName};

    #[test]
    fn test_lfn_empty() {
        let lfn = LongFileName::new();
        assert!(lfn.is_empty());
        assert_eq!(lfn.to_string(), "");
    }

    #[test]
    fn test_lfn_builder_start() {
        let mut builder = LfnBuilder::new();
        // Start with sequence number 0x41 (first and last entry)
        builder.start(0x41, 0x12);
        assert!(builder.building);
    }

    #[test]
    fn test_lfn_prepend_ascii() {
        let mut lfn = LongFileName::new();

        // Create LFN entry with "test" encoded as UTF-16LE
        // "test" = 't' 'e' 's' 't' + padding
        let name1: [u8; 10] = [
            b't', 0, b'e', 0, b's', 0, b't', 0, 0x00, 0x00, // "test" + null terminator
        ];
        let name2: [u8; 12] = [0xFF; 12]; // Padding
        let name3: [u8; 4] = [0xFF; 4]; // Padding

        lfn.prepend_lfn_entry(&name1, &name2, &name3);

        assert_eq!(lfn.to_string(), "test");
    }

    #[test]
    fn test_lfn_prepend_multiple() {
        let mut lfn = LongFileName::new();

        // Second part: "file.txt"
        let name1_2: [u8; 10] = [b'f', 0, b'i', 0, b'l', 0, b'e', 0, b'.', 0];
        let name2_2: [u8; 12] = [
            b't', 0, b'x', 0, b't', 0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
        ];
        let name3_2: [u8; 4] = [0xFF; 4];

        lfn.prepend_lfn_entry(&name1_2, &name2_2, &name3_2);

        // First part: "long_"
        let name1_1: [u8; 10] = [b'l', 0, b'o', 0, b'n', 0, b'g', 0, b'_', 0];
        let name2_1: [u8; 12] = [0xFF; 12];
        let name3_1: [u8; 4] = [0xFF; 4];

        lfn.prepend_lfn_entry(&name1_1, &name2_1, &name3_1);

        assert_eq!(lfn.to_string(), "long_file.txt");
    }
}

// Integration tests using in-memory FAT32 images

#[cfg(feature = "write")]
mod integration_tests {
    use hadris_fat::format::{FatFormatOptions, FatVolumeFormatter};
    use hadris_fat::{FatVolume, FatVolumeWriteExt};
    use std::io::Cursor;

    /// Create a test FAT32 image with known directory structure:
    /// /
    /// ├── HELLO.TXT     (content: "Hello, World!")
    /// ├── DATA.BIN      (content: 1024 bytes of 0xAA)
    /// ├── SUBDIR/
    /// │   ├── NESTED.TXT (content: "Nested file content")
    /// │   └── DEEP/
    /// │       └── FILE.TXT (content: "Deep file")
    pub fn create_test_fat32_image() -> Cursor<Vec<u8>> {
        // Use a 4MB volume (small but sufficient for FAT32)
        let volume_size: u64 = 4 * 1024 * 1024;
        let buffer = vec![0u8; volume_size as usize];
        let mut cursor = Cursor::new(buffer);

        let opts = FatFormatOptions::new(volume_size);
        let fs =
            FatVolumeFormatter::format(&mut cursor, opts).expect("Failed to format FAT32 volume");

        // Get root directory
        let root = fs.root_dir();

        // Create HELLO.TXT
        let hello_entry = fs.create_file(&root, "HELLO.TXT").unwrap();
        let mut hello_writer = fs.write_file(&hello_entry).unwrap();
        hello_writer.write(b"Hello, World!").unwrap();
        hello_writer.finish().unwrap();

        // Create DATA.BIN with 1024 bytes of 0xAA
        let data_entry = fs.create_file(&root, "DATA.BIN").unwrap();
        let mut data_writer = fs.write_file(&data_entry).unwrap();
        let data_content = vec![0xAA; 1024];
        data_writer.write(&data_content).unwrap();
        data_writer.finish().unwrap();

        // Create SUBDIR
        let subdir = fs.create_dir(&root, "SUBDIR").unwrap();

        // Create SUBDIR/NESTED.TXT
        let nested_entry = fs.create_file(&subdir, "NESTED.TXT").unwrap();
        let mut nested_writer = fs.write_file(&nested_entry).unwrap();
        nested_writer.write(b"Nested file content").unwrap();
        nested_writer.finish().unwrap();

        // Create SUBDIR/DEEP
        let deep_dir = fs.create_dir(&subdir, "DEEP").unwrap();

        // Create SUBDIR/DEEP/FILE.TXT
        let file_entry = fs.create_file(&deep_dir, "FILE.TXT").unwrap();
        let mut file_writer = fs.write_file(&file_entry).unwrap();
        file_writer.write(b"Deep file").unwrap();
        file_writer.finish().unwrap();

        // Sync to ensure all changes are written
        fs.sync().unwrap();

        cursor
    }

    #[test]
    fn test_root_listing_skips_volume_label_entry() {
        use hadris_fat::format::{FatFormatOptions, FatTypeSelection, FatVolumeFormatter};

        let volume_size: u64 = 256 * 1024 * 1024;
        let buffer = vec![0u8; volume_size as usize];
        let mut cursor = Cursor::new(buffer);

        let opts = FatFormatOptions::new(volume_size)
            .volume_label("PMOS_BOOT")
            .fat_type(FatTypeSelection::Fat32);
        let fs = FatVolumeFormatter::format(&mut cursor, opts).expect("format FAT32");

        let entries: Vec<_> = fs.root_dir().entries().filter_map(|e| e.ok()).collect();
        assert!(
            entries.is_empty(),
            "volume label must not appear in directory listing"
        );
        assert_eq!(
            fs.read_root_label().expect("read_root_label"),
            Some(*b"PMOS_BOOT  ")
        );
    }

    #[test]
    fn test_root_listing_skips_mkfs_style_lowercase_label() {
        use hadris_fat::format::{FatFormatOptions, FatTypeSelection, FatVolumeFormatter};
        use hadris_io::Seek;

        let volume_size: u64 = 256 * 1024 * 1024;
        let mut buffer = vec![0u8; volume_size as usize];
        let mut cursor = Cursor::new(&mut buffer);

        let opts = FatFormatOptions::new(volume_size)
            .volume_label("PLACEHOLDER")
            .fat_type(FatTypeSelection::Fat32);
        {
            let fs = FatVolumeFormatter::format(&mut cursor, opts).expect("format FAT32");
            // mkfs.fat stores the label verbatim, including lowercase (issue #31).
            fs.set_root_label(b"pmOS_boot  ").expect("set_root_label");
        }

        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();
        let fs = FatVolume::open(cursor).expect("re-open FAT32");

        let entries: Vec<_> = fs.root_dir().entries().filter_map(|e| e.ok()).collect();
        assert!(
            entries.is_empty(),
            "lowercase mkfs.fat-style volume label must not break listing"
        );
        assert_eq!(
            fs.read_root_label().expect("read_root_label"),
            Some(*b"pmOS_boot  ")
        );
    }

    #[test]
    fn test_read_directory_entries() {
        use hadris_io::Seek;

        let mut cursor = create_test_fat32_image();
        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

        let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");
        let root = fs.root_dir();

        // Collect all entries (excluding . and ..)
        let entries: Vec<_> = root
            .entries()
            .filter_map(|e| e.ok())
            .filter(|e| {
                let name = e.name();
                name != "." && name != ".."
            })
            .collect();

        // Should have 3 entries: HELLO.TXT, DATA.BIN, SUBDIR
        assert_eq!(entries.len(), 3, "Expected 3 entries in root directory");

        // Verify file names
        let names: Vec<_> = entries.iter().map(|e| e.name()).collect();
        assert!(
            names.iter().any(|n| n.starts_with("HELLO")),
            "Should find HELLO.TXT"
        );
        assert!(
            names.iter().any(|n| n.starts_with("DATA")),
            "Should find DATA.BIN"
        );
        assert!(
            names.iter().any(|n| n.starts_with("SUBDIR")),
            "Should find SUBDIR"
        );
    }

    #[test]
    fn test_read_file_contents() {
        use hadris_io::Seek;

        let mut cursor = create_test_fat32_image();
        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

        let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");
        let root = fs.root_dir();

        // Read HELLO.TXT
        let mut hello_reader = root.open_file("HELLO.TXT").unwrap();
        let hello_content = hello_reader.read_to_vec().unwrap();
        assert_eq!(String::from_utf8(hello_content).unwrap(), "Hello, World!");

        // Read DATA.BIN
        let mut data_reader = root.open_file("DATA.BIN").unwrap();
        let data_content = data_reader.read_to_vec().unwrap();
        assert_eq!(data_content.len(), 1024);
        assert!(data_content.iter().all(|&b| b == 0xAA));
    }

    #[test]
    #[cfg(feature = "lfn")]
    fn test_read_lfn_entries() {
        use hadris_io::Seek;

        // Note: The current write API only creates short filenames (8.3 format).
        // The LFN feature is primarily for reading existing LFN entries created
        // by other tools. This test verifies that the LFN parsing infrastructure
        // compiles and works correctly by testing with short filenames.
        let mut cursor = create_test_fat32_image();
        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

        let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");
        let root = fs.root_dir();

        // Verify we can read the short filenames (8.3 format)
        let result = root.find("HELLO.TXT");
        assert!(result.is_ok());
        assert!(result.unwrap().is_some(), "Should find HELLO.TXT");

        // Verify directory iteration works with LFN feature enabled
        let entries: Vec<_> = root
            .entries()
            .filter_map(|e| e.ok())
            .filter(|e| {
                let name = e.name();
                name != "." && name != ".."
            })
            .collect();

        // Should have 3 entries: HELLO.TXT, DATA.BIN, SUBDIR
        assert_eq!(entries.len(), 3, "Expected 3 entries in root directory");
    }
}

#[cfg(test)]
#[cfg(feature = "write")]
mod navigation_tests {
    use super::integration_tests::create_test_fat32_image;
    use hadris_fat::{Error, FatVolume};
    use hadris_io::Seek;

    /// Test that find() returns None for non-existent entries
    #[test]
    fn test_find_nonexistent_returns_none() {
        let mut cursor = create_test_fat32_image();
        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

        let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");
        let root = fs.root_dir();

        // Try to find a non-existent file
        let result = root.find("NONEXISTENT.TXT");
        assert!(result.is_ok());
        assert!(result.unwrap().is_none());
    }

    /// Test that open_dir() returns NotADirectory error for files
    #[test]
    fn test_open_dir_on_file_returns_error() {
        let mut cursor = create_test_fat32_image();
        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

        let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");
        let root = fs.root_dir();

        // Try to open a file as a directory
        let result = root.open_dir("HELLO.TXT");
        assert!(result.is_err());
        match result {
            Err(Error::NotADirectory) => {}
            _ => panic!("Expected NotADirectory error"),
        }
    }

    /// Test that open_file() returns NotAFile error for directories
    #[test]
    fn test_open_file_on_directory_returns_error() {
        let mut cursor = create_test_fat32_image();
        cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

        let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");
        let root = fs.root_dir();

        // Try to open a directory as a file
        let result = root.open_file("SUBDIR");
        assert!(result.is_err());
        match result {
            Err(Error::NotAFile) => {}
            _ => panic!("Expected NotAFile error"),
        }
    }

    /// Test error variants display correctly
    #[test]
    fn test_error_display() {
        let err = Error::EntryNotFound;
        assert_eq!(format!("{err}"), "entry not found in directory");

        let err = Error::InvalidPath;
        assert_eq!(format!("{err}"), "path is invalid (empty or malformed)");
    }

    /// Test path-based API with invalid paths
    #[cfg(feature = "alloc")]
    mod path_tests {
        use hadris_fat::Error;

        #[test]
        fn test_invalid_path_empty() {
            // Even if we can't open a real filesystem, we can verify the error type exists
            // and the API surface is correct through compilation
            let _: Result<(), Error> = Err(Error::InvalidPath);
            let _: Result<(), Error> = Err(Error::EntryNotFound);
        }

        #[test]
        #[cfg(feature = "write")]
        fn test_open_path_empty_returns_invalid() {
            use super::super::integration_tests::create_test_fat32_image;
            use hadris_fat::FatVolume;
            use hadris_io::Seek;

            let mut cursor = create_test_fat32_image();
            cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

            let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");

            // Try to open an empty path
            let result = fs.open_path("");
            assert!(result.is_err());
            match result {
                Err(Error::InvalidPath) => {}
                _ => panic!("Expected InvalidPath error"),
            }
        }

        #[test]
        #[cfg(feature = "write")]
        fn test_open_path_slash_only_returns_invalid() {
            use super::super::integration_tests::create_test_fat32_image;
            use hadris_fat::FatVolume;
            use hadris_io::Seek;

            let mut cursor = create_test_fat32_image();
            cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

            let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");

            // Try to open a slash-only path
            let result = fs.open_path("/");
            assert!(result.is_err());
            match result {
                Err(Error::InvalidPath) => {}
                _ => panic!("Expected InvalidPath error"),
            }
        }

        #[test]
        #[cfg(feature = "write")]
        fn test_open_path_traversal() {
            use super::super::integration_tests::create_test_fat32_image;
            use hadris_fat::FatVolume;
            use hadris_io::Seek;

            let mut cursor = create_test_fat32_image();
            cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

            let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");

            // Test multi-level path navigation
            let result = fs.open_path("SUBDIR/DEEP/FILE.TXT");
            assert!(result.is_ok(), "Path traversal should work");

            let entry = result.unwrap();
            assert!(entry.is_file());
            assert_eq!(entry.len(), 9); // "Deep file" is 9 bytes
        }

        #[test]
        #[cfg(feature = "write")]
        fn test_open_file_path() {
            use super::super::integration_tests::create_test_fat32_image;
            use hadris_fat::FatVolume;
            use hadris_io::Seek;

            let mut cursor = create_test_fat32_image();
            cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

            let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");

            // Test opening a file by path
            let mut reader = fs.open_file_path("SUBDIR/NESTED.TXT").unwrap();
            let content = reader.read_to_vec().unwrap();
            assert_eq!(String::from_utf8(content).unwrap(), "Nested file content");
        }

        #[test]
        #[cfg(feature = "write")]
        fn test_open_dir_path() {
            use super::super::integration_tests::create_test_fat32_image;
            use hadris_fat::FatVolume;
            use hadris_io::Seek;

            let mut cursor = create_test_fat32_image();
            cursor.seek(std::io::SeekFrom::Start(0).into()).unwrap();

            let fs = FatVolume::open(cursor).expect("Failed to open FAT32 image");

            // Test opening a directory by path
            let dir = fs.open_dir_path("SUBDIR/DEEP").unwrap();

            // Verify we can list entries in the directory
            let entries: Vec<_> = dir
                .entries()
                .filter_map(|e| e.ok())
                .filter(|e| {
                    let name = e.name();
                    name != "." && name != ".."
                })
                .collect();

            // Should have 1 entry: FILE.TXT
            assert_eq!(entries.len(), 1);
            assert!(entries[0].name().starts_with("FILE"));
        }
    }
}