diff --git a/ext/phar/pharzip.h b/ext/phar/pharzip.h index 2daca5b339966..5c814747b6923 100644 --- a/ext/phar/pharzip.h +++ b/ext/phar/pharzip.h @@ -146,6 +146,13 @@ typedef struct _phar_zip_unix3 { /* (var.) variable symbolic link filename */ } phar_zip_unix3; +/* See https://libzip.org/specifications/extrafld.txt */ +typedef struct _phar_zip_unix_time { + phar_zip_extra_field_header header; + char flags; /* flags 1 byte */ + char time[4]; /* time in standard Unix format 4 bytes */ +} phar_zip_unix_time; + typedef struct _phar_zip_central_dir_file { char signature[4]; /* central file header signature 4 bytes (0x02014b50) */ char madeby[2]; /* version made by 2 bytes */ diff --git a/ext/phar/tests/gh12532.phpt b/ext/phar/tests/gh12532.phpt new file mode 100644 index 0000000000000..2c1419198e897 --- /dev/null +++ b/ext/phar/tests/gh12532.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-12532 (PharData created from zip has incorrect timestamp) +--EXTENSIONS-- +phar +--FILE-- +getMTime(), "\n"; +echo $phar->getFileInfo()->getMTime(), "\n"; +echo date('Y-m-d H:i:s', $phar->getMTime()), "\n"; +echo date('Y-m-d H:i:s', $phar->getCTime()), "\n"; +echo date('Y-m-d H:i:s', $phar->getATime()), "\n"; + +?> +--EXPECT-- +1680284661 +1680284661 +2023-03-31 17:44:21 +2023-03-31 17:44:21 +2023-03-31 17:44:21 diff --git a/ext/phar/tests/gh12532.zip b/ext/phar/tests/gh12532.zip new file mode 100644 index 0000000000000..9b52086eb29ca Binary files /dev/null and b/ext/phar/tests/gh12532.zip differ diff --git a/ext/phar/zip.c b/ext/phar/zip.c index 675cb7f616f0a..2c690e9310ebc 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -44,6 +44,7 @@ static int phar_zip_process_extra(php_stream *fp, phar_entry_info *entry, uint16 union { phar_zip_extra_field_header header; phar_zip_unix3 unix3; + phar_zip_unix_time time; } h; size_t read; @@ -52,6 +53,35 @@ static int phar_zip_process_extra(php_stream *fp, phar_entry_info *entry, uint16 return FAILURE; } + if (h.header.tag[0] == 'U' && h.header.tag[1] == 'T') { + /* Unix timestamp header found. + * The flags field indicates which timestamp fields are present. + * The size with a timestamp is at least 5 (== 9 - tag size) bytes, but may be larger. + * We only store the modification time in the entry, so only read that. + */ + const size_t min_size = 5; + uint16_t header_size = PHAR_GET_16(h.header.size); + if (header_size >= min_size) { + read = php_stream_read(fp, &h.time.flags, min_size); + if (read != min_size) { + return FAILURE; + } + if (h.time.flags & (1 << 0)) { + /* Modification time set */ + entry->timestamp = PHAR_GET_32(h.time.time); + } + + len -= header_size + 4; + + /* Consume remaining bytes */ + if (header_size != read) { + php_stream_seek(fp, header_size - read, SEEK_CUR); + } + continue; + } + /* Fallthrough to next if to skip header */ + } + if (h.header.tag[0] != 'n' || h.header.tag[1] != 'u') { /* skip to next header */ php_stream_seek(fp, PHAR_GET_16(h.header.size), SEEK_CUR);