From 7340f0768dcd23918397e07cce5874e327808ab8 Mon Sep 17 00:00:00 2001 From: Evan Sarkar Date: Mon, 29 Jun 2026 01:00:54 +0530 Subject: [PATCH] feat(core): add public FatAttributes constants (R/H/S/A) Expose a public, named FatAttributes object for the FAT directory-entry attribute bits (the byte at entry offset 0x0B) so callers of Fat12Volume.setAttributes and readers of Fat12Entry.attributes no longer have to hard-code magic numbers like 0x01 / 0x02 / 0x04 / 0x20. The four user-settable bits (READ_ONLY, HIDDEN, SYSTEM, ARCHIVE) plus the read-only VOLUME_ID / DIRECTORY bits are exposed with KDoc citing the FAT spec offset. The setAttributes KDoc now references the named constants. Adds FatAttributesTest covering the bit values, or-combination, and a round-trip through setAttributes -> Fat12Entry.attributes. Fixes #1 --- .../com/ams/fat12ex/core/Fat12Volume.kt | 9 +-- .../com/ams/fat12ex/core/FatAttributes.kt | 33 +++++++++ .../com/ams/fat12ex/core/FatAttributesTest.kt | 71 +++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 core/src/main/kotlin/com/ams/fat12ex/core/FatAttributes.kt create mode 100644 core/src/test/kotlin/com/ams/fat12ex/core/FatAttributesTest.kt diff --git a/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Volume.kt b/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Volume.kt index c546eaf..9ed0f76 100644 --- a/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Volume.kt +++ b/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Volume.kt @@ -745,10 +745,11 @@ class Fat12Volume(private val device: BlockDevice) : Closeable { * [path] IN PLACE under the INT-02 verify-after-write + rollback contract * (structurally modelled on [setVolumeLabel]). * - * Only the four user bits are written: READ_ONLY (0x01), HIDDEN (0x02), - * SYSTEM (0x04), ARCHIVE (0x20) — the [USER_ATTR_MASK]. The non-user bits - * ATTR_VOLUME_ID (0x08) and ATTR_DIRECTORY (0x10) — the [PRESERVE_ATTR_MASK] - * — are PRESERVED from the existing byte, so a caller can never flip a + * Only the four user bits are written: [FatAttributes.READ_ONLY] (0x01), + * [FatAttributes.HIDDEN] (0x02), [FatAttributes.SYSTEM] (0x04), + * [FatAttributes.ARCHIVE] (0x20) — the [USER_ATTR_MASK]. The non-user bits + * [FatAttributes.VOLUME_ID] (0x08) and [FatAttributes.DIRECTORY] (0x10) — the + * [PRESERVE_ATTR_MASK] — are PRESERVED from the existing byte, so a caller can never flip a * folder into a file, clear the directory bit, or set the volume-ID bit * (Pitfall 4 / T-05-01). The LFN composite (0x0F) never reaches here because * [findEntryByName] resolves only short entries. diff --git a/core/src/main/kotlin/com/ams/fat12ex/core/FatAttributes.kt b/core/src/main/kotlin/com/ams/fat12ex/core/FatAttributes.kt new file mode 100644 index 0000000..ca064c8 --- /dev/null +++ b/core/src/main/kotlin/com/ams/fat12ex/core/FatAttributes.kt @@ -0,0 +1,33 @@ +package com.ams.fat12ex.core + +/** + * Public FAT directory-entry attribute bits — the attribute byte at directory-entry + * offset `0x0B` (per the FAT specification). + * + * These let callers of [Fat12Volume.setAttributes] and readers of [Fat12Entry.attributes] + * use named constants instead of hard-coding magic numbers like `0x01` / `0x02`. The + * four user-settable bits ([READ_ONLY], [HIDDEN], [SYSTEM], [ARCHIVE]) are the only ones + * `setAttributes` writes; [VOLUME_ID] and [DIRECTORY] are exposed read-only for callers + * inspecting [Fat12Entry.attributes] (the engine preserves them — see [Fat12Volume.setAttributes]). + * + * Bits combine with `or`, e.g. `READ_ONLY or HIDDEN == 0x03`. + */ +object FatAttributes { + /** Read-only (R) — bit 0. */ + const val READ_ONLY: Int = 0x01 + + /** Hidden (H) — bit 1. */ + const val HIDDEN: Int = 0x02 + + /** System (S) — bit 2. */ + const val SYSTEM: Int = 0x04 + + /** Volume-label entry (read-only here; never user-settable via [Fat12Volume.setAttributes]). */ + const val VOLUME_ID: Int = 0x08 + + /** Directory entry (read-only here; preserved by [Fat12Volume.setAttributes]). */ + const val DIRECTORY: Int = 0x10 + + /** Archive (A) — bit 5. */ + const val ARCHIVE: Int = 0x20 +} diff --git a/core/src/test/kotlin/com/ams/fat12ex/core/FatAttributesTest.kt b/core/src/test/kotlin/com/ams/fat12ex/core/FatAttributesTest.kt new file mode 100644 index 0000000..32fe736 --- /dev/null +++ b/core/src/test/kotlin/com/ams/fat12ex/core/FatAttributesTest.kt @@ -0,0 +1,71 @@ +package com.ams.fat12ex.core + +import com.ams.fat12ex.core.testutil.Fat12ImageBuilder +import com.ams.fat12ex.core.testutil.InMemoryBlockDevice +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +/** + * [FatAttributes] tests — the public, named FAT directory-entry attribute bits. + * + * Proves the bit values match the FAT spec (the attribute byte at entry offset + * 0x0B), that they combine with `or` as documented, and that they round-trip + * through [Fat12Volume.setAttributes] -> [Fat12Entry.attributes]. + */ +class FatAttributesTest { + + // ----- case 1: the constants equal their FAT-spec bit values ------------- + + @Test + fun bitValues_matchTheFatSpec() { + assertEquals(0x01, FatAttributes.READ_ONLY) + assertEquals(0x02, FatAttributes.HIDDEN) + assertEquals(0x04, FatAttributes.SYSTEM) + assertEquals(0x08, FatAttributes.VOLUME_ID) + assertEquals(0x10, FatAttributes.DIRECTORY) + assertEquals(0x20, FatAttributes.ARCHIVE) + } + + // ----- case 2: bits combine with `or` ------------------------------------- + + @Test + fun bits_combineWithOr() { + assertEquals(0x03, FatAttributes.READ_ONLY or FatAttributes.HIDDEN) + assertEquals(0x07, FatAttributes.READ_ONLY or FatAttributes.HIDDEN or FatAttributes.SYSTEM) + assertEquals(0x21, FatAttributes.READ_ONLY or FatAttributes.ARCHIVE) + // The four user bits together are the engine's USER_ATTR_MASK (0x27). + val userBits = FatAttributes.READ_ONLY or FatAttributes.HIDDEN or + FatAttributes.SYSTEM or FatAttributes.ARCHIVE + assertEquals(0x27, userBits) + } + + // ----- case 3: the constants round-trip through setAttributes ------------- + + @Test + fun constants_roundTripThroughSetAttributes() { + val device: InMemoryBlockDevice = + Fat12ImageBuilder() + .withReservedShortEntry( + name83 = "DATA BIN", + attr = FatAttributes.ARCHIVE, + clusters = listOf(2), + bytes = ByteArray(16) { it.toByte() }, + ) + .build() + val vol = Fat12Volume(device).apply { open() } + + assertInstanceOf( + Fat12Result.Ok::class.java, + vol.setAttributes("/DATA.BIN", FatAttributes.READ_ONLY or FatAttributes.HIDDEN), + ) + + val entry = (vol.list("") as Fat12Result.Ok).value + .first { it.name.equals("DATA.BIN", ignoreCase = true) } + assertTrue((entry.attributes and FatAttributes.READ_ONLY) != 0, "R must be set") + assertTrue((entry.attributes and FatAttributes.HIDDEN) != 0, "H must be set") + assertEquals(0, entry.attributes and FatAttributes.SYSTEM, "S must be clear") + assertEquals(0, entry.attributes and FatAttributes.ARCHIVE, "A must have been cleared") + } +}