* For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PhpZip\Model\Extra\Fields; use PhpZip\Exception\InvalidArgumentException; use PhpZip\Exception\ZipException; use PhpZip\Model\Extra\ZipExtraField; use PhpZip\Model\ZipEntry; /** * NTFS Extra Field. * * @see https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT .ZIP File Format Specification */ final class NtfsExtraField implements ZipExtraField { /** @var int Header id */ public const HEADER_ID = 0x000A; /** @var int Tag ID */ public const TIME_ATTR_TAG = 0x0001; /** @var int Attribute size */ public const TIME_ATTR_SIZE = 24; // 3 * 8 /** * @var int A file time is a 64-bit value that represents the number of * 100-nanosecond intervals that have elapsed since 12:00 * A.M. January 1, 1601 Coordinated Universal Time (UTC). * this is the offset of Windows time 0 to Unix epoch in 100-nanosecond intervals. */ public const EPOCH_OFFSET = -116444736000000000; /** @var int Modify ntfs time */ private int $modifyNtfsTime; /** @var int Access ntfs time */ private int $accessNtfsTime; /** @var int Create ntfs time */ private int $createNtfsTime; public function __construct(int $modifyNtfsTime, int $accessNtfsTime, int $createNtfsTime) { $this->modifyNtfsTime = $modifyNtfsTime; $this->accessNtfsTime = $accessNtfsTime; $this->createNtfsTime = $createNtfsTime; } /** * @return NtfsExtraField */ public static function create( \DateTimeInterface $modifyDateTime, \DateTimeInterface $accessDateTime, \DateTimeInterface $createNtfsTime ): self { return new self( self::dateTimeToNtfsTime($modifyDateTime), self::dateTimeToNtfsTime($accessDateTime), self::dateTimeToNtfsTime($createNtfsTime) ); } /** * Returns the Header ID (type) of this Extra Field. * The Header ID is an unsigned short integer (two bytes) * which must be constant during the life cycle of this object. */ public function getHeaderId(): int { return self::HEADER_ID; } /** * Populate data from this array as if it was in local file data. * * @param string $buffer the buffer to read data from * @param ZipEntry|null $entry optional zip entry * * @throws ZipException * * @return NtfsExtraField */ public static function unpackLocalFileData(string $buffer, ?ZipEntry $entry = null): self { if (\PHP_INT_SIZE === 4) { throw new ZipException('not supported for php-32bit'); } $buffer = substr($buffer, 4); $modifyTime = 0; $accessTime = 0; $createTime = 0; while ($buffer || $buffer !== '') { [ 'tag' => $tag, 'sizeAttr' => $sizeAttr, ] = unpack('vtag/vsizeAttr', $buffer); if ($tag === self::TIME_ATTR_TAG && $sizeAttr === self::TIME_ATTR_SIZE) { [ 'modifyTime' => $modifyTime, 'accessTime' => $accessTime, 'createTime' => $createTime, ] = unpack('PmodifyTime/PaccessTime/PcreateTime', substr($buffer, 4, 24)); break; } $buffer = substr($buffer, 4 + $sizeAttr); } return new self($modifyTime, $accessTime, $createTime); } /** * Populate data from this array as if it was in central directory data. * * @param string $buffer the buffer to read data from * @param ZipEntry|null $entry optional zip entry * * @throws ZipException * * @return NtfsExtraField */ public static function unpackCentralDirData(string $buffer, ?ZipEntry $entry = null): self { return self::unpackLocalFileData($buffer, $entry); } /** * The actual data to put into local file data - without Header-ID * or length specifier. * * @return string the data */ public function packLocalFileData(): string { return pack( 'VvvPPP', 0, self::TIME_ATTR_TAG, self::TIME_ATTR_SIZE, $this->modifyNtfsTime, $this->accessNtfsTime, $this->createNtfsTime ); } public function getModifyNtfsTime(): int { return $this->modifyNtfsTime; } public function setModifyNtfsTime(int $modifyNtfsTime): void { $this->modifyNtfsTime = $modifyNtfsTime; } public function getAccessNtfsTime(): int { return $this->accessNtfsTime; } public function setAccessNtfsTime(int $accessNtfsTime): void { $this->accessNtfsTime = $accessNtfsTime; } public function getCreateNtfsTime(): int { return $this->createNtfsTime; } public function setCreateNtfsTime(int $createNtfsTime): void { $this->createNtfsTime = $createNtfsTime; } /** * The actual data to put into central directory - without Header-ID or * length specifier. * * @return string the data */ public function packCentralDirData(): string { return $this->packLocalFileData(); } public function getModifyDateTime(): \DateTimeInterface { return self::ntfsTimeToDateTime($this->modifyNtfsTime); } public function setModifyDateTime(\DateTimeInterface $modifyTime): void { $this->modifyNtfsTime = self::dateTimeToNtfsTime($modifyTime); } public function getAccessDateTime(): \DateTimeInterface { return self::ntfsTimeToDateTime($this->accessNtfsTime); } public function setAccessDateTime(\DateTimeInterface $accessTime): void { $this->accessNtfsTime = self::dateTimeToNtfsTime($accessTime); } public function getCreateDateTime(): \DateTimeInterface { return self::ntfsTimeToDateTime($this->createNtfsTime); } public function setCreateDateTime(\DateTimeInterface $createTime): void { $this->createNtfsTime = self::dateTimeToNtfsTime($createTime); } /** * @param float $timestamp Float timestamp */ public static function timestampToNtfsTime(float $timestamp): int { return (int) (($timestamp * 10000000) - self::EPOCH_OFFSET); } public static function dateTimeToNtfsTime(\DateTimeInterface $dateTime): int { return self::timestampToNtfsTime((float) $dateTime->format('U.u')); } /** * @return float Float unix timestamp */ public static function ntfsTimeToTimestamp(int $ntfsTime): float { return (float) (($ntfsTime + self::EPOCH_OFFSET) / 10000000); } public static function ntfsTimeToDateTime(int $ntfsTime): \DateTimeInterface { $timestamp = self::ntfsTimeToTimestamp($ntfsTime); $dateTime = \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6f', $timestamp)); if ($dateTime === false) { throw new InvalidArgumentException('Cannot create date/time object for timestamp ' . $timestamp); } return $dateTime; } public function __toString(): string { $args = [self::HEADER_ID]; $format = '0x%04x NtfsExtra:'; if ($this->modifyNtfsTime !== 0) { $format .= ' Modify:[%s]'; $args[] = $this->getModifyDateTime()->format(\DATE_ATOM); } if ($this->accessNtfsTime !== 0) { $format .= ' Access:[%s]'; $args[] = $this->getAccessDateTime()->format(\DATE_ATOM); } if ($this->createNtfsTime !== 0) { $format .= ' Create:[%s]'; $args[] = $this->getCreateDateTime()->format(\DATE_ATOM); } return vsprintf($format, $args); } }