feat(09-01): update Kotlin decoder for v1.1 format with directory support
- Add entryType and permissions fields to TocEntry data class - Parse entry_type (1 byte) and permissions (2 bytes LE) in parseTocEntry - Update version check from 1 to 2 for v1.1 format - Handle directory entries: create dirs without decryption - Create parent directories for files with relative paths - Add applyPermissions() using Java File API (owner vs everyone) - Update entry size formula comment to 104 + name_length Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,11 @@ data class ArchiveHeader(
|
||||
val tocIv: ByteArray,
|
||||
)
|
||||
|
||||
/** File table entry (variable length: 101 + name_length bytes). FORMAT.md Section 5. */
|
||||
/** Entry table entry (variable length: 104 + name_length bytes). FORMAT.md Section 5 (v1.1). */
|
||||
data class TocEntry(
|
||||
val name: String,
|
||||
val entryType: Int, // 0x00=file, 0x01=directory
|
||||
val permissions: Int, // Lower 12 bits of POSIX mode_t
|
||||
val originalSize: Long,
|
||||
val compressedSize: Long,
|
||||
val encryptedSize: Int,
|
||||
@@ -94,7 +96,7 @@ fun readLeU32(data: ByteArray, offset: Int): Long {
|
||||
/**
|
||||
* Parse the 40-byte archive header.
|
||||
*
|
||||
* Verifies: magic bytes, version == 1, reserved flag bits 4-7 are zero.
|
||||
* Verifies: magic bytes, version == 2 (v1.1 format), reserved flag bits 4-7 are zero.
|
||||
*/
|
||||
fun parseHeader(data: ByteArray): ArchiveHeader {
|
||||
require(data.size >= HEADER_SIZE) { "Header too short: ${data.size} bytes" }
|
||||
@@ -107,7 +109,7 @@ fun parseHeader(data: ByteArray): ArchiveHeader {
|
||||
|
||||
// Version check
|
||||
val version = data[4].toInt() and 0xFF
|
||||
require(version == 1) { "Unsupported version: $version" }
|
||||
require(version == 2) { "Unsupported version: $version (expected v1.1 format, version=2)" }
|
||||
|
||||
// Flags validation
|
||||
val flags = data[5].toInt() and 0xFF
|
||||
@@ -130,7 +132,7 @@ fun parseHeader(data: ByteArray): ArchiveHeader {
|
||||
* Parse a single TOC entry from [data] starting at [offset].
|
||||
*
|
||||
* Returns a Pair of the parsed entry and the new offset after the entry.
|
||||
* Entry size formula: 101 + name_length bytes.
|
||||
* Entry size formula: 104 + name_length bytes (v1.1).
|
||||
*/
|
||||
fun parseTocEntry(data: ByteArray, offset: Int): Pair<TocEntry, Int> {
|
||||
var pos = offset
|
||||
@@ -143,6 +145,12 @@ fun parseTocEntry(data: ByteArray, offset: Int): Pair<TocEntry, Int> {
|
||||
val name = String(data, pos, nameLength, Charsets.UTF_8)
|
||||
pos += nameLength
|
||||
|
||||
// entry_type (u8): 0x00=file, 0x01=directory (v1.1)
|
||||
val entryType = data[pos].toInt() and 0xFF; pos += 1
|
||||
|
||||
// permissions (u16 LE): lower 12 bits of POSIX mode_t (v1.1)
|
||||
val permissions = readLeU16(data, pos); pos += 2
|
||||
|
||||
// Fixed fields: original_size, compressed_size, encrypted_size, data_offset (all u32 LE)
|
||||
val originalSize = readLeU32(data, pos); pos += 4
|
||||
val compressedSize = readLeU32(data, pos); pos += 4
|
||||
@@ -165,7 +173,7 @@ fun parseTocEntry(data: ByteArray, offset: Int): Pair<TocEntry, Int> {
|
||||
val paddingAfter = readLeU16(data, pos); pos += 2
|
||||
|
||||
val entry = TocEntry(
|
||||
name, originalSize, compressedSize, encryptedSize,
|
||||
name, entryType, permissions, originalSize, compressedSize, encryptedSize,
|
||||
dataOffset, iv, hmac, sha256, compressionFlag, paddingAfter
|
||||
)
|
||||
return Pair(entry, pos)
|
||||
@@ -269,6 +277,37 @@ fun xorHeader(buf: ByteArray) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permissions restoration (v1.1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Apply POSIX permissions to a file or directory using Java File API.
|
||||
*
|
||||
* Java's File API can only distinguish "owner" vs "everyone" permissions
|
||||
* (not owner/group/others separately). This is acceptable per KOT-07.
|
||||
*
|
||||
* @param file The file or directory to apply permissions to.
|
||||
* @param permissions Lower 12 bits of POSIX mode_t (e.g., 0o755 = 0x01ED).
|
||||
*/
|
||||
fun applyPermissions(file: File, permissions: Int) {
|
||||
val ownerRead = (permissions shr 8) and 1 != 0 // bit 8
|
||||
val ownerWrite = (permissions shr 7) and 1 != 0 // bit 7
|
||||
val ownerExec = (permissions shr 6) and 1 != 0 // bit 6
|
||||
val othersRead = (permissions shr 2) and 1 != 0 // bit 2
|
||||
val othersWrite = (permissions shr 1) and 1 != 0 // bit 1
|
||||
val othersExec = permissions and 1 != 0 // bit 0
|
||||
|
||||
// Set "everyone" permissions first (ownerOnly=false), then override owner-only
|
||||
file.setReadable(othersRead, false)
|
||||
file.setWritable(othersWrite, false)
|
||||
file.setExecutable(othersExec, false)
|
||||
// Owner-only overrides (ownerOnly=true)
|
||||
file.setReadable(ownerRead, true)
|
||||
file.setWritable(ownerWrite, true)
|
||||
file.setExecutable(ownerExec, true)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main decode orchestration (FORMAT.md Section 10)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -317,6 +356,22 @@ fun decode(archivePath: String, outputDir: String) {
|
||||
var successCount = 0
|
||||
|
||||
for (entry in entries) {
|
||||
if (entry.entryType == 1) {
|
||||
// Directory entry: create the directory, apply permissions, no decryption
|
||||
val dir = File(outputDir, entry.name)
|
||||
dir.mkdirs()
|
||||
applyPermissions(dir, entry.permissions)
|
||||
println("Created dir: ${entry.name}")
|
||||
successCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// File entry (entryType == 0): standard crypto pipeline
|
||||
|
||||
// Ensure parent directories exist (for files with relative paths)
|
||||
val outFile = File(outputDir, entry.name)
|
||||
outFile.parentFile?.mkdirs()
|
||||
|
||||
// Step 1: Seek to data_offset and read ciphertext
|
||||
raf.seek(entry.dataOffset)
|
||||
val ciphertext = ByteArray(entry.encryptedSize)
|
||||
@@ -344,16 +399,16 @@ fun decode(archivePath: String, outputDir: String) {
|
||||
// Still write the file (matching Rust behavior)
|
||||
}
|
||||
|
||||
// Step 6: Write output file
|
||||
val outFile = File(outputDir, entry.name)
|
||||
// Step 6: Write output file and apply permissions
|
||||
outFile.writeBytes(original)
|
||||
applyPermissions(outFile, entry.permissions)
|
||||
println("Extracted: ${entry.name} (${original.size} bytes)")
|
||||
successCount++
|
||||
}
|
||||
|
||||
raf.close()
|
||||
|
||||
println("Done: $successCount files extracted")
|
||||
println("Done: $successCount entries extracted")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user