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:
NikitolProject
2026-02-26 22:04:54 +03:00
parent e905269bb5
commit a01b260944

View File

@@ -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")
}
// ---------------------------------------------------------------------------