Skip to content

Commit

Permalink
WIP: NTFS timestamp support
Browse files Browse the repository at this point in the history
  • Loading branch information
swankjesse committed Mar 2, 2024
1 parent 20b83aa commit 8f3f015
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 4 deletions.
67 changes: 63 additions & 4 deletions okio/src/zlibMain/kotlin/okio/internal/ZipFiles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ private const val BIT_FLAG_UNSUPPORTED_MASK = BIT_FLAG_ENCRYPTED
private const val MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0xffffffffL

private const val HEADER_ID_ZIP64_EXTENDED_INFO = 0x1
private const val HEADER_ID_NTFS_EXTRA = 0x000a
private const val HEADER_ID_EXTENDED_TIMESTAMP = 0x5455

/**
Expand Down Expand Up @@ -228,12 +229,20 @@ internal fun BufferedSource.readEntry(): ZipEntry {
}

var hasZip64Extra = false
var hasNtfsExtra = false
readExtra(extraSize) { headerId, dataSize ->
when (headerId) {
HEADER_ID_NTFS_EXTRA -> {
if (hasNtfsExtra) throw IOException("bad zip: NTFS extra repeated")
hasNtfsExtra = true
val ntfsExtra = readNtfsExtra(dataSize)
println("modificationTimeNtUnits=${ntfsExtra?.modificationTimeNtUnits}")
println("lastAccessTimeNtUnits=${ntfsExtra?.lastAccessTimeNtUnits}")
println("creationTimeNtUnits=${ntfsExtra?.creationTimeNtUnits}")
}

HEADER_ID_ZIP64_EXTENDED_INFO -> {
if (hasZip64Extra) {
throw IOException("bad zip: zip64 extra repeated")
}
if (hasZip64Extra) throw IOException("bad zip: zip64 extra repeated")
hasZip64Extra = true

if (dataSize < requiredZip64ExtraSize) {
Expand Down Expand Up @@ -269,6 +278,50 @@ internal fun BufferedSource.readEntry(): ZipEntry {
)
}

/**
* Reads the NTFS extra metadata. This metadata recursively does a tag and length scheme inside of
* ZIP extras' own tag and length scheme. So we reuse [readExtra] and do that again.
*
* The only attribute we're interested in is attribute 0x0001, which has 3 timestamps:
*
* * 8 byte modification time
* * 8 byte last access time
* * 8 byte creation time
*
* All three are in the NT timestamp format.
* https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
*/
private fun BufferedSource.readNtfsExtra(
ntfsExtraSize: Long,
): NtfsExtraAttributeTag0? {
var ntfsExtraAttributeTag0: NtfsExtraAttributeTag0? = null

if (ntfsExtraSize < 4L) throw IOException("bad zip: NTFS extra too short")
skip(4L)

readExtra((ntfsExtraSize - 4L).toInt()) { headerId, dataSize ->
when (headerId) {
0x1 -> {
if (ntfsExtraAttributeTag0 != null) {
throw IOException("bad zip: NTFS extra attribute tag 0x0001 repeated")
}

if (dataSize != 24L) {
throw IOException("bad zip: NTFS extra attribute tag 0x0001 size != 24")
}

ntfsExtraAttributeTag0 = NtfsExtraAttributeTag0(
modificationTimeNtUnits = readLongLe(),
lastAccessTimeNtUnits = readLongLe(),
creationTimeNtUnits = readLongLe(),
)
}
}
}

return ntfsExtraAttributeTag0
}

@Throws(IOException::class)
private fun BufferedSource.readEocdRecord(): EocdRecord {
val diskNumber = readShortLe().toInt() and 0xffff
Expand Down Expand Up @@ -356,7 +409,7 @@ internal fun BufferedSource.readLocalHeader(basicMetadata: FileMetadata): FileMe
}

/**
* If [basicMetadata] is null this will return null. Otherwise it will return a new header which
* If [basicMetadata] is null this will return null. Otherwise, it will return a new header which
* updates [basicMetadata] with information from the local header.
*/
private fun BufferedSource.readOrSkipLocalHeader(basicMetadata: FileMetadata?): FileMetadata? {
Expand Down Expand Up @@ -444,6 +497,12 @@ private fun dosDateTimeToEpochMillis(date: Int, time: Int): Long? {
)
}

private class NtfsExtraAttributeTag0(
val modificationTimeNtUnits: Long,
val lastAccessTimeNtUnits: Long,
val creationTimeNtUnits: Long,
)

private class EocdRecord(
val entryCount: Long,
val centralDirectoryOffset: Long,
Expand Down
9 changes: 9 additions & 0 deletions okio/src/zlibTest/kotlin/okio/ZipFileSystemTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,15 @@ class ZipFileSystemTest {
zipFileSystem.canonicalize("not/a/path".toPath())
}
}

@Test
fun ntfs() {
val zipPath =
"/Users/jwilson/.rvm/gems/ruby-2.7.4/gems/rubyzip-1.3.0/test/data/ntfs.zip".toPath()
val zipFileSystem = fileSystem.openZip(zipPath)

println(zipFileSystem.list("/".toPath()))
}
}

private fun ByteString.replaceAll(a: ByteString, b: ByteString): ByteString {
Expand Down

0 comments on commit 8f3f015

Please sign in to comment.