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,
|
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(
|
data class TocEntry(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val entryType: Int, // 0x00=file, 0x01=directory
|
||||||
|
val permissions: Int, // Lower 12 bits of POSIX mode_t
|
||||||
val originalSize: Long,
|
val originalSize: Long,
|
||||||
val compressedSize: Long,
|
val compressedSize: Long,
|
||||||
val encryptedSize: Int,
|
val encryptedSize: Int,
|
||||||
@@ -94,7 +96,7 @@ fun readLeU32(data: ByteArray, offset: Int): Long {
|
|||||||
/**
|
/**
|
||||||
* Parse the 40-byte archive header.
|
* 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 {
|
fun parseHeader(data: ByteArray): ArchiveHeader {
|
||||||
require(data.size >= HEADER_SIZE) { "Header too short: ${data.size} bytes" }
|
require(data.size >= HEADER_SIZE) { "Header too short: ${data.size} bytes" }
|
||||||
@@ -107,7 +109,7 @@ fun parseHeader(data: ByteArray): ArchiveHeader {
|
|||||||
|
|
||||||
// Version check
|
// Version check
|
||||||
val version = data[4].toInt() and 0xFF
|
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
|
// Flags validation
|
||||||
val flags = data[5].toInt() and 0xFF
|
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].
|
* Parse a single TOC entry from [data] starting at [offset].
|
||||||
*
|
*
|
||||||
* Returns a Pair of the parsed entry and the new offset after the entry.
|
* 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> {
|
fun parseTocEntry(data: ByteArray, offset: Int): Pair<TocEntry, Int> {
|
||||||
var pos = offset
|
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)
|
val name = String(data, pos, nameLength, Charsets.UTF_8)
|
||||||
pos += nameLength
|
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)
|
// Fixed fields: original_size, compressed_size, encrypted_size, data_offset (all u32 LE)
|
||||||
val originalSize = readLeU32(data, pos); pos += 4
|
val originalSize = readLeU32(data, pos); pos += 4
|
||||||
val compressedSize = 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 paddingAfter = readLeU16(data, pos); pos += 2
|
||||||
|
|
||||||
val entry = TocEntry(
|
val entry = TocEntry(
|
||||||
name, originalSize, compressedSize, encryptedSize,
|
name, entryType, permissions, originalSize, compressedSize, encryptedSize,
|
||||||
dataOffset, iv, hmac, sha256, compressionFlag, paddingAfter
|
dataOffset, iv, hmac, sha256, compressionFlag, paddingAfter
|
||||||
)
|
)
|
||||||
return Pair(entry, pos)
|
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)
|
// Main decode orchestration (FORMAT.md Section 10)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -317,6 +356,22 @@ fun decode(archivePath: String, outputDir: String) {
|
|||||||
var successCount = 0
|
var successCount = 0
|
||||||
|
|
||||||
for (entry in entries) {
|
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
|
// Step 1: Seek to data_offset and read ciphertext
|
||||||
raf.seek(entry.dataOffset)
|
raf.seek(entry.dataOffset)
|
||||||
val ciphertext = ByteArray(entry.encryptedSize)
|
val ciphertext = ByteArray(entry.encryptedSize)
|
||||||
@@ -344,16 +399,16 @@ fun decode(archivePath: String, outputDir: String) {
|
|||||||
// Still write the file (matching Rust behavior)
|
// Still write the file (matching Rust behavior)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Write output file
|
// Step 6: Write output file and apply permissions
|
||||||
val outFile = File(outputDir, entry.name)
|
|
||||||
outFile.writeBytes(original)
|
outFile.writeBytes(original)
|
||||||
|
applyPermissions(outFile, entry.permissions)
|
||||||
println("Extracted: ${entry.name} (${original.size} bytes)")
|
println("Extracted: ${entry.name} (${original.size} bytes)")
|
||||||
successCount++
|
successCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
raf.close()
|
raf.close()
|
||||||
|
|
||||||
println("Done: $successCount files extracted")
|
println("Done: $successCount entries extracted")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user