Skip to content

Commit

Permalink
Get ZipFileSystem to prefer NTFS timestamps when present (#1449)
Browse files Browse the repository at this point in the history
* Get ZipFileSystem to prefer NTFS timestamps when present

They don't have a time bomb in 2038.

* Add a missing import

But why didn't the compiler get mad at me?
  • Loading branch information
squarejesse authored Mar 4, 2024
1 parent 2c0d99e commit 940496a
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 103 deletions.
5 changes: 5 additions & 0 deletions okio-testing-support/src/commonMain/resources/go/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The files in this directory are copied from Go:
https://go.dev/

These files are subject to the 3-Clause BSD License:
https://github.com/golang/go/blob/master/LICENSE
Binary file not shown.
Binary file not shown.
69 changes: 20 additions & 49 deletions okio/src/zlibMain/kotlin/okio/ZipFileSystem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,37 +26,6 @@ import okio.internal.skipLocalHeader
/**
* Read only access to a [zip file][zip_format] and common [extra fields][extra_fields].
*
* Zip Timestamps
* --------------
*
* The base zip format tracks the [last modified timestamp][FileMetadata.lastModifiedAtMillis]. It
* does not track [created timestamps][FileMetadata.createdAtMillis] or [last accessed
* timestamps][FileMetadata.lastAccessedAtMillis]. This format has limitations:
*
* * Timestamps are 16-bit values stored with 2-second precision. Some zip encoders (WinZip, PKZIP)
* round up to the nearest 2 seconds; other encoders (Java) round down.
*
* * Timestamps before 1980-01-01 cannot be represented. They cannot represent dates after
* 2107-12-31.
*
* * Timestamps are stored in local time with no time zone offset. If the time zone offset changes
* – due to daylight savings time or the zip file being sent to another time zone – file times
* will be incorrect. The file time will be shifted by the difference in time zone offsets
* between the encoder and decoder.
*
* The zip format has optional extensions for timestamps.
*
* * UNIX timestamps (0x000d) support both last-access time and last modification time. These
* timestamps are stored with 1-second precision using UTC.
*
* * NTFS timestamps (0x000a) support creation time, last access time, and last modified time.
* These timestamps are stored with 100-millisecond precision using UTC.
*
* * 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.
*
* [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 All @@ -81,27 +50,29 @@ internal class ZipFileSystem internal constructor(

override fun metadataOrNull(path: Path): FileMetadata? {
val canonicalPath = canonicalizeInternal(path)
val entry = entries[canonicalPath] ?: return null

val basicMetadata = FileMetadata(
isRegularFile = !entry.isDirectory,
isDirectory = entry.isDirectory,
symlinkTarget = null,
size = if (entry.isDirectory) null else entry.size,
createdAtMillis = null,
lastModifiedAtMillis = entry.lastModifiedAtMillis,
lastAccessedAtMillis = null,
)
val centralDirectoryEntry = entries[canonicalPath] ?: return null

val fullEntry = when {
centralDirectoryEntry.offset != -1L -> {
fileSystem.openReadOnly(zipPath).use { fileHandle ->
return@use fileHandle.source(centralDirectoryEntry.offset).buffer().use { source ->
source.readLocalHeader(centralDirectoryEntry)
}
}
}

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

return fileSystem.openReadOnly(zipPath).use { fileHandle ->
return@use fileHandle.source(entry.offset).buffer().use { source ->
source.readLocalHeader(basicMetadata)
}
}
return FileMetadata(
isRegularFile = !fullEntry.isDirectory,
isDirectory = fullEntry.isDirectory,
symlinkTarget = null,
size = if (fullEntry.isDirectory) null else fullEntry.size,
createdAtMillis = fullEntry.createdAtMillis,
lastModifiedAtMillis = fullEntry.lastModifiedAtMillis,
lastAccessedAtMillis = fullEntry.lastAccessedAtMillis,
)
}

override fun openReadOnly(file: Path): FileHandle {
Expand Down
90 changes: 88 additions & 2 deletions okio/src/zlibMain/kotlin/okio/internal/ZipEntry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
*/
package okio.internal

import okio.FileMetadata
import okio.Path

/**
* This class prefers NTFS timestamps, then extended timestamps, then the base ZIP timestamps.
*/
internal class ZipEntry(
/**
* Absolute path of this entry. If the raw name on disk contains relative paths like `..`, they
Expand All @@ -43,9 +47,91 @@ internal class ZipEntry(
/** Either [COMPRESSION_METHOD_DEFLATED] or [COMPRESSION_METHOD_STORED]. */
val compressionMethod: Int = -1,

val lastModifiedAtMillis: Long? = null,

val offset: Long = -1L,

/**
* The base ZIP format tracks the [last modified timestamp][FileMetadata.lastModifiedAtMillis]. It
* does not track [created timestamps][FileMetadata.createdAtMillis] or [last accessed
* timestamps][FileMetadata.lastAccessedAtMillis].
*
* This format has severe limitations:
*
* * Timestamps are 16-bit values stored with 2-second precision. Some zip encoders (WinZip,
* PKZIP) round up to the nearest 2 seconds; other encoders (Java) round down.
*
* * Timestamps before 1980-01-01 cannot be represented. They cannot represent dates after
* 2107-12-31.
*
* * Timestamps are stored in local time with no time zone offset. If the time zone offset
* changes – due to daylight savings time or the zip file being sent to another time zone –
* file times will be incorrect. The file time will be shifted by the difference in time zone
* offsets between the encoder and decoder.
*/
val dosLastModifiedAtDate: Int = -1,
val dosLastModifiedAtTime: Int = -1,

/**
* NTFS timestamps (0x000a) support creation time, last access time, and last modified time.
* These timestamps are stored with 100-millisecond precision using UTC.
*/
val ntfsLastModifiedAtFiletime: Long? = null,
val ntfsLastAccessedAtFiletime: Long? = null,
val ntfsCreatedAtFiletime: Long? = null,

/**
* Extended timestamps (0x5455) are stored as signed 32-bit timestamps with 1-second precision.
* These cannot express dates beyond 2038-01-19.
*/
val extendedLastModifiedAtSeconds: Int? = null,
val extendedLastAccessedAtSeconds: Int? = null,
val extendedCreatedAtSeconds: Int? = null,
) {
val children = mutableListOf<Path>()

internal fun copy(
extendedLastModifiedAtSeconds: Int?,
extendedLastAccessedAtSeconds: Int?,
extendedCreatedAtSeconds: Int?,
) = ZipEntry(
canonicalPath = canonicalPath,
isDirectory = isDirectory,
comment = comment,
crc = crc,
compressedSize = compressedSize,
size = size,
compressionMethod = compressionMethod,
offset = offset,
dosLastModifiedAtDate = dosLastModifiedAtDate,
dosLastModifiedAtTime = dosLastModifiedAtTime,
ntfsLastModifiedAtFiletime = ntfsLastModifiedAtFiletime,
ntfsLastAccessedAtFiletime = ntfsLastAccessedAtFiletime,
ntfsCreatedAtFiletime = ntfsCreatedAtFiletime,
extendedLastModifiedAtSeconds = extendedLastModifiedAtSeconds,
extendedLastAccessedAtSeconds = extendedLastAccessedAtSeconds,
extendedCreatedAtSeconds = extendedCreatedAtSeconds,
)

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

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

internal val createdAtMillis: Long?
get() = when {
ntfsCreatedAtFiletime != null -> filetimeToEpochMillis(ntfsCreatedAtFiletime)
extendedCreatedAtSeconds != null -> extendedCreatedAtSeconds * 1000L
else -> null
}
}
Loading

0 comments on commit 940496a

Please sign in to comment.