Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
swankjesse committed Mar 3, 2024
1 parent 20b83aa commit 3d7ba61
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 82 deletions.
Binary file not shown.
19 changes: 4 additions & 15 deletions okio/src/zlibMain/kotlin/okio/ZipFileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import okio.Path.Companion.toPath
import okio.internal.COMPRESSION_METHOD_STORED
import okio.internal.FixedLengthSource
import okio.internal.ZipEntry
import okio.internal.readLocalHeader
import okio.internal.skipLocalHeader

/**
Expand Down Expand Up @@ -55,7 +54,7 @@ import okio.internal.skipLocalHeader
* * Extended timestamps (0x5455) are stored as signed 32-bit timestamps with 1-second precision.
* These cannot express dates beyond 2038-01-19.
*
* This class currently supports base timestamps and extended timestamps.
* This class prefers NTFS timestamps, then extended timestamps, then the base zip timestamps.
*
* [zip_format]: https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE_6.2.0.txt
* [extra_fields]: https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld
Expand Down Expand Up @@ -83,25 +82,15 @@ internal class ZipFileSystem internal constructor(
val canonicalPath = canonicalizeInternal(path)
val entry = entries[canonicalPath] ?: return null

val basicMetadata = FileMetadata(
return FileMetadata(
isRegularFile = !entry.isDirectory,
isDirectory = entry.isDirectory,
symlinkTarget = null,
size = if (entry.isDirectory) null else entry.size,
createdAtMillis = null,
createdAtMillis = entry.createdAtMillis,
lastModifiedAtMillis = entry.lastModifiedAtMillis,
lastAccessedAtMillis = null,
lastAccessedAtMillis = entry.lastAccessedAtMillis,
)

if (entry.offset == -1L) {
return basicMetadata
}

return fileSystem.openReadOnly(zipPath).use { fileHandle ->
return@use fileHandle.source(entry.offset).buffer().use { source ->
source.readLocalHeader(basicMetadata)
}
}
}

override fun openReadOnly(file: Path): FileHandle {
Expand Down
35 changes: 34 additions & 1 deletion okio/src/zlibMain/kotlin/okio/internal/ZipEntry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,42 @@ internal class ZipEntry(
/** Either [COMPRESSION_METHOD_DEFLATED] or [COMPRESSION_METHOD_STORED]. */
val compressionMethod: Int = -1,

val lastModifiedAtMillis: Long? = null,
val dosLastModifiedAtDate: Int = -1,
val dosLastModifiedAtTime: Int = -1,

val unixLastModifiedAtSeconds: Int? = null,
val unixLastAccessedAtSeconds: Int? = null,
val unixCreatedAtSeconds: Int? = null,

val ntfsModificationFiletime: Long? = null,
val ntfsLastAccessFiletime: Long? = null,
val ntfsCreationFiletime: Long? = null,

val offset: Long = -1L,
) {
val children = mutableListOf<Path>()

internal val lastAccessedAtMillis: Long?
get() = when {
ntfsLastAccessFiletime != null -> filetimeToEpochMillis(ntfsLastAccessFiletime)
unixLastAccessedAtSeconds != null -> unixLastAccessedAtSeconds * 1000L
else -> null
}

internal val lastModifiedAtMillis: Long?
get() = when {
ntfsModificationFiletime != null -> filetimeToEpochMillis(ntfsModificationFiletime)
unixLastModifiedAtSeconds != null -> unixLastModifiedAtSeconds * 1000L
dosLastModifiedAtTime != -1 -> {
dosDateTimeToEpochMillis(dosLastModifiedAtDate, dosLastModifiedAtTime)
}
else -> null
}

internal val createdAtMillis: Long?
get() = when {
ntfsCreationFiletime != null -> filetimeToEpochMillis(ntfsCreationFiletime)
unixCreatedAtSeconds != null -> unixCreatedAtSeconds * 1000L
else -> null
}
}
150 changes: 84 additions & 66 deletions okio/src/zlibMain/kotlin/okio/internal/ZipFiles.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package okio.internal

import okio.BufferedSource
import okio.FileMetadata
import okio.FileSystem
import okio.IOException
import okio.Path
Expand Down Expand Up @@ -48,6 +47,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 @@ -201,10 +201,8 @@ internal fun BufferedSource.readEntry(): ZipEntry {
}

val compressionMethod = readShortLe().toInt() and 0xffff
val time = readShortLe().toInt() and 0xffff
val date = readShortLe().toInt() and 0xffff
// TODO(jwilson): decode NTFS and UNIX extra metadata to return better timestamps.
val lastModifiedAtMillis = dosDateTimeToEpochMillis(date, time)
val dosLastModifiedTime = readShortLe().toInt() and 0xffff
val dosLastModifiedDate = readShortLe().toInt() and 0xffff

// These are 32-bit values in the file, but 64-bit fields in this object.
val crc = readIntLe().toLong() and 0xffffffffL
Expand All @@ -227,6 +225,14 @@ internal fun BufferedSource.readEntry(): ZipEntry {
return@run result
}

var unixLastModifiedAtSeconds: Int? = null
var unixLastAccessedAtSeconds: Int? = null
var unixCreatedAtSeconds: Int? = null

var ntfsModificationFiletime: Long? = null
var ntfsLastAccessFiletime: Long? = null
var ntfsCreationFiletime: Long? = null

var hasZip64Extra = false
readExtra(extraSize) { headerId, dataSize ->
when (headerId) {
Expand All @@ -245,6 +251,58 @@ internal fun BufferedSource.readEntry(): ZipEntry {
compressedSize = if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else 0L
offset = if (offset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) readLongLe() else 0L
}

HEADER_ID_NTFS_EXTRA -> {
if (dataSize < 4L) {
throw IOException("bad zip: NTFS extra too short")
}
skip(4L)

// 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 do readExtra() again.
readExtra((dataSize - 4L).toInt()) { attributeId, attributeSize ->
when (attributeId) {
0x1 -> {
if (ntfsModificationFiletime != null) {
throw IOException("bad zip: NTFS extra attribute tag 0x0001 repeated")
}

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

ntfsModificationFiletime = readLongLe()
ntfsLastAccessFiletime = readLongLe()
ntfsCreationFiletime = readLongLe()
}
}
}
}

HEADER_ID_EXTENDED_TIMESTAMP -> {
if (dataSize < 1) {
throw IOException("bad zip: extended timestamp extra too short")
}
val flags = readByte().toInt() and 0xff

val hasLastModifiedAtMillis = (flags and 0x1) == 0x1
val hasLastAccessedAtMillis = (flags and 0x2) == 0x2
val hasCreatedAtMillis = (flags and 0x4) == 0x4
val requiredSize = run {
var result = 1L
if (hasLastModifiedAtMillis) result += 4L
if (hasLastAccessedAtMillis) result += 4L
if (hasCreatedAtMillis) result += 4L
return@run result
}
if (dataSize < requiredSize) {
throw IOException("bad zip: extended timestamp extra too short")
}

if (hasLastModifiedAtMillis) unixLastModifiedAtSeconds = readIntLe()
if (hasLastAccessedAtMillis) unixLastAccessedAtSeconds = readIntLe()
if (hasCreatedAtMillis) unixCreatedAtSeconds = readIntLe()
}
}
}

Expand All @@ -264,7 +322,14 @@ internal fun BufferedSource.readEntry(): ZipEntry {
compressedSize = compressedSize,
size = size,
compressionMethod = compressionMethod,
lastModifiedAtMillis = lastModifiedAtMillis,
dosLastModifiedAtDate = dosLastModifiedDate,
dosLastModifiedAtTime = dosLastModifiedTime,
unixLastModifiedAtSeconds = unixLastModifiedAtSeconds,
unixLastAccessedAtSeconds = unixLastAccessedAtSeconds,
unixCreatedAtSeconds = unixCreatedAtSeconds,
ntfsModificationFiletime = ntfsModificationFiletime,
ntfsLastAccessFiletime = ntfsLastAccessFiletime,
ntfsCreationFiletime = ntfsCreationFiletime,
offset = offset,
)
}
Expand Down Expand Up @@ -348,22 +413,6 @@ private fun BufferedSource.readExtra(extraSize: Int, block: (Int, Long) -> Unit)
}

internal fun BufferedSource.skipLocalHeader() {
readOrSkipLocalHeader(null)
}

internal fun BufferedSource.readLocalHeader(basicMetadata: FileMetadata): FileMetadata {
return readOrSkipLocalHeader(basicMetadata)!!
}

/**
* 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? {
var lastModifiedAtMillis = basicMetadata?.lastModifiedAtMillis
var lastAccessedAtMillis: Long? = null
var createdAtMillis: Long? = null

val signature = readIntLe()
if (signature != LOCAL_FILE_HEADER_SIGNATURE) {
throw IOException(
Expand All @@ -379,57 +428,26 @@ private fun BufferedSource.readOrSkipLocalHeader(basicMetadata: FileMetadata?):
val fileNameLength = readShortLe().toLong() and 0xffff
val extraSize = readShortLe().toInt() and 0xffff
skip(fileNameLength)
skip(extraSize.toLong())
}

if (basicMetadata == null) {
skip(extraSize.toLong())
return null
}

readExtra(extraSize) { headerId, dataSize ->
when (headerId) {
HEADER_ID_EXTENDED_TIMESTAMP -> {
if (dataSize < 1) {
throw IOException("bad zip: extended timestamp extra too short")
}
val flags = readByte().toInt() and 0xff

val hasLastModifiedAtMillis = (flags and 0x1) == 0x1
val hasLastAccessedAtMillis = (flags and 0x2) == 0x2
val hasCreatedAtMillis = (flags and 0x4) == 0x4
val requiredSize = run {
var result = 1L
if (hasLastModifiedAtMillis) result += 4L
if (hasLastAccessedAtMillis) result += 4L
if (hasCreatedAtMillis) result += 4L
return@run result
}
if (dataSize < requiredSize) {
throw IOException("bad zip: extended timestamp extra too short")
}

if (hasLastModifiedAtMillis) lastModifiedAtMillis = readIntLe() * 1000L
if (hasLastAccessedAtMillis) lastAccessedAtMillis = readIntLe() * 1000L
if (hasCreatedAtMillis) createdAtMillis = readIntLe() * 1000L
}
}
}

return FileMetadata(
isRegularFile = basicMetadata.isRegularFile,
isDirectory = basicMetadata.isDirectory,
symlinkTarget = null,
size = basicMetadata.size,
createdAtMillis = createdAtMillis,
lastModifiedAtMillis = lastModifiedAtMillis,
lastAccessedAtMillis = lastAccessedAtMillis,
)
/**
* Converts from the Microsoft [filetime] format to the Java epoch millis format.
*
* * Filetime's unit is 100 nanoseconds, and 0 is 1601-01-01T00:00:00Z.
* * Java epoch millis' unit is 1 millisecond, and 0 is 1970-01-01T00:00:00Z.
*
* See also https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime
*/
internal fun filetimeToEpochMillis(filetime: Long): Long {
return filetime
}

/**
* Converts a 32-bit DOS date+time to milliseconds since epoch. Note that this function interprets
* a value with no time zone as a value with the local time zone.
*/
private fun dosDateTimeToEpochMillis(date: Int, time: Int): Long? {
internal fun dosDateTimeToEpochMillis(date: Int, time: Int): Long? {
if (time == -1) {
return null
}
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 3d7ba61

Please sign in to comment.