Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions core/src/main/kotlin/com/ams/fat12ex/core/Fat12Volume.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions core/src/main/kotlin/com/ams/fat12ex/core/FatAttributes.kt
Original file line number Diff line number Diff line change
@@ -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
}
71 changes: 71 additions & 0 deletions core/src/test/kotlin/com/ams/fat12ex/core/FatAttributesTest.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading