ExtendedTimestampExtraField.php 13.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
<?php

namespace PhpZip\Model\Extra\Fields;

use PhpZip\Model\Extra\ZipExtraField;
use PhpZip\Model\ZipEntry;

/**
 * Extended Timestamp Extra Field:
 * ==============================.
 *
 * The following is the layout of the extended-timestamp extra block.
 * (Last Revision 19970118)
 *
 * Local-header version:
 *
 * Value         Size        Description
 * -----         ----        -----------
 * (time) 0x5455 Short       tag for this extra block type ("UT")
 * TSize         Short       total data size for this block
 * Flags         Byte        info bits
 * (ModTime)     Long        time of last modification (UTC/GMT)
 * (AcTime)      Long        time of last access (UTC/GMT)
 * (CrTime)      Long        time of original creation (UTC/GMT)
 *
 * Central-header version:
 *
 * Value         Size        Description
 * -----         ----        -----------
 * (time) 0x5455 Short       tag for this extra block type ("UT")
 * TSize         Short       total data size for this block
 * Flags         Byte        info bits (refers to local header!)
 * (ModTime)     Long        time of last modification (UTC/GMT)
 *
 * The central-header extra field contains the modification time only,
 * or no timestamp at all.  TSize is used to flag its presence or
 * absence.  But note:
 *
 * If "Flags" indicates that Modtime is present in the local header
 * field, it MUST be present in the central header field, too!
 * This correspondence is required because the modification time
 * value may be used to support trans-timezone freshening and
 * updating operations with zip archives.
 *
 * The time values are in standard Unix signed-long format, indicating
 * the number of seconds since 1 January 1970 00:00:00.  The times
 * are relative to Coordinated Universal Time (UTC), also sometimes
 * referred to as Greenwich Mean Time (GMT).  To convert to local time,
 * the software must know the local timezone offset from UTC/GMT.
 *
 * The lower three bits of Flags in both headers indicate which time-
 * stamps are present in the LOCAL extra field:
 *
 * bit 0           if set, modification time is present
 * bit 1           if set, access time is present
 * bit 2           if set, creation time is present
 * bits 3-7        reserved for additional timestamps; not set
 *
 * Those times that are present will appear in the order indicated, but
 * any combination of times may be omitted.  (Creation time may be
 * present without access time, for example.)  TSize should equal
 * (1 + 4*(number of set bits in Flags)), as the block is currently
 * defined.  Other timestamps may be added in the future.
 *
 * @see ftp://ftp.info-zip.org/pub/infozip/doc/appnote-iz-latest.zip Info-ZIP version Specification
 */
class ExtendedTimestampExtraField implements ZipExtraField
{
    /** @var int Header id */
    const HEADER_ID = 0x5455;

    /**
     * @var int the bit set inside the flags by when the last modification time
     *          is present in this extra field
     */
    const MODIFY_TIME_BIT = 1;

    /**
     * @var int the bit set inside the flags by when the last access time is
     *          present in this extra field
     */
    const ACCESS_TIME_BIT = 2;

    /**
     * @var int the bit set inside the flags by when the original creation time
     *          is present in this extra field
     */
    const CREATE_TIME_BIT = 4;

    /**
     * @var int The 3 boolean fields (below) come from this flags byte.  The remaining 5 bits
     *          are ignored according to the current version of the spec (December 2012).
     */
    private $flags;

    /** @var int|null Modify time */
    private $modifyTime;

    /** @var int|null Access time */
    private $accessTime;

    /** @var int|null Create time */
    private $createTime;

    /**
     * @param int      $flags
     * @param int|null $modifyTime
     * @param int|null $accessTime
     * @param int|null $createTime
     */
    public function __construct($flags, $modifyTime, $accessTime, $createTime)
    {
        $this->flags = (int) $flags;
        $this->modifyTime = $modifyTime;
        $this->accessTime = $accessTime;
        $this->createTime = $createTime;
    }

    /**
     * @param int|null $modifyTime
     * @param int|null $accessTime
     * @param int|null $createTime
     *
     * @return ExtendedTimestampExtraField
     */
    public static function create($modifyTime, $accessTime, $createTime)
    {
        $flags = 0;

        if ($modifyTime !== null) {
            $modifyTime = (int) $modifyTime;
            $flags |= self::MODIFY_TIME_BIT;
        }

        if ($accessTime !== null) {
            $accessTime = (int) $accessTime;
            $flags |= self::ACCESS_TIME_BIT;
        }

        if ($createTime !== null) {
            $createTime = (int) $createTime;
            $flags |= self::CREATE_TIME_BIT;
        }

        return new self($flags, $modifyTime, $accessTime, $createTime);
    }

    /**
     * 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.
     *
     * @return int
     */
    public function getHeaderId()
    {
        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
     *
     * @return ExtendedTimestampExtraField
     */
    public static function unpackLocalFileData($buffer, ZipEntry $entry = null)
    {
        $length = \strlen($buffer);
        $flags = unpack('C', $buffer)[1];
        $offset = 1;

        $modifyTime = null;
        $accessTime = null;
        $createTime = null;

        if (($flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT) {
            $modifyTime = unpack('V', substr($buffer, $offset, 4))[1];
            $offset += 4;
        }

        // Notice the extra length check in case we are parsing the shorter
        // central data field (for both access and create timestamps).
        if ((($flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT) && $offset + 4 <= $length) {
            $accessTime = unpack('V', substr($buffer, $offset, 4))[1];
            $offset += 4;
        }

        if ((($flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT) && $offset + 4 <= $length) {
            $createTime = unpack('V', substr($buffer, $offset, 4))[1];
        }

        return new self($flags, $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
     *
     * @return ExtendedTimestampExtraField
     */
    public static function unpackCentralDirData($buffer, ZipEntry $entry = null)
    {
        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()
    {
        $data = '';

        if (($this->flags & self::MODIFY_TIME_BIT) === self::MODIFY_TIME_BIT && $this->modifyTime !== null) {
            $data .= pack('V', $this->modifyTime);
        }

        if (($this->flags & self::ACCESS_TIME_BIT) === self::ACCESS_TIME_BIT && $this->accessTime !== null) {
            $data .= pack('V', $this->accessTime);
        }

        if (($this->flags & self::CREATE_TIME_BIT) === self::CREATE_TIME_BIT && $this->createTime !== null) {
            $data .= pack('V', $this->createTime);
        }

        return pack('C', $this->flags) . $data;
    }

    /**
     * The actual data to put into central directory - without Header-ID or
     * length specifier.
     *
     * Note: even if bit1 and bit2 are set, the Central data will still
     * not contain access/create fields: only local data ever holds those!
     *
     * @return string the data
     */
    public function packCentralDirData()
    {
        $cdLength = 1 + ($this->modifyTime !== null ? 4 : 0);

        return substr($this->packLocalFileData(), 0, $cdLength);
    }

    /**
     * Gets flags byte.
     *
     * The flags byte tells us which of the three datestamp fields are
     * present in the data:
     * bit0 - modify time
     * bit1 - access time
     * bit2 - create time
     *
     * Only first 3 bits of flags are used according to the
     * latest version of the spec (December 2012).
     *
     * @return int flags byte indicating which of the
     *             three datestamp fields are present
     */
    public function getFlags()
    {
        return $this->flags;
    }

    /**
     * Returns the modify time (seconds since epoch) of this zip entry,
     * or null if no such timestamp exists in the zip entry.
     *
     * @return int|null modify time (seconds since epoch) or null
     */
    public function getModifyTime()
    {
        return $this->modifyTime;
    }

    /**
     * Returns the access time (seconds since epoch) of this zip entry,
     * or null if no such timestamp exists in the zip entry.
     *
     * @return int|null access time (seconds since epoch) or null
     */
    public function getAccessTime()
    {
        return $this->accessTime;
    }

    /**
     * Returns the create time (seconds since epoch) of this zip entry,
     * or null if no such timestamp exists in the zip entry.
     *
     * Note: modern linux file systems (e.g., ext2)
     * do not appear to store a "create time" value, and so
     * it's usually omitted altogether in the zip extra
     * field. Perhaps other unix systems track this.
     *
     * @return int|null create time (seconds since epoch) or null
     */
    public function getCreateTime()
    {
        return $this->createTime;
    }

    /**
     * Returns the modify time as a \DateTimeInterface
     * of this zip entry, or null if no such timestamp exists in the zip entry.
     * The milliseconds are always zeroed out, since the underlying data
     * offers only per-second precision.
     *
     * @return \DateTimeInterface|null modify time as \DateTimeInterface or null
     */
    public function getModifyDateTime()
    {
        return self::timestampToDateTime($this->modifyTime);
    }

    /**
     * Returns the access time as a \DateTimeInterface
     * of this zip entry, or null if no such timestamp exists in the zip entry.
     * The milliseconds are always zeroed out, since the underlying data
     * offers only per-second precision.
     *
     * @return \DateTimeInterface|null access time as \DateTimeInterface or null
     */
    public function getAccessDateTime()
    {
        return self::timestampToDateTime($this->accessTime);
    }

    /**
     * Returns the create time as a a \DateTimeInterface
     * of this zip entry, or null if no such timestamp exists in the zip entry.
     * The milliseconds are always zeroed out, since the underlying data
     * offers only per-second precision.
     *
     * Note: modern linux file systems (e.g., ext2)
     * do not appear to store a "create time" value, and so
     * it's usually omitted altogether in the zip extra
     * field.  Perhaps other unix systems track $this->.
     *
     * @return \DateTimeInterface|null create time as \DateTimeInterface or null
     */
    public function getCreateDateTime()
    {
        return self::timestampToDateTime($this->createTime);
    }

    /**
     * Sets the modify time (seconds since epoch) of this zip entry
     * using a integer.
     *
     * @param int|null $unixTime unix time of the modify time (seconds per epoch) or null
     */
    public function setModifyTime($unixTime)
    {
        $this->modifyTime = $unixTime;
        $this->updateFlags();
    }

    private function updateFlags()
    {
        $flags = 0;

        if ($this->modifyTime !== null) {
            $flags |= self::MODIFY_TIME_BIT;
        }

        if ($this->accessTime !== null) {
            $flags |= self::ACCESS_TIME_BIT;
        }

        if ($this->createTime !== null) {
            $flags |= self::CREATE_TIME_BIT;
        }
        $this->flags = $flags;
    }

    /**
     * Sets the access time (seconds since epoch) of this zip entry
     * using a integer.
     *
     * @param int|null $unixTime Unix time of the access time (seconds per epoch) or null
     */
    public function setAccessTime($unixTime)
    {
        $this->accessTime = $unixTime;
        $this->updateFlags();
    }

    /**
     * Sets the create time (seconds since epoch) of this zip entry
     * using a integer.
     *
     * @param int|null $unixTime Unix time of the create time (seconds per epoch) or null
     */
    public function setCreateTime($unixTime)
    {
        $this->createTime = $unixTime;
        $this->updateFlags();
    }

    /**
     * @param int|null $timestamp
     *
     * @return \DateTimeInterface|null
     */
    private static function timestampToDateTime($timestamp)
    {
        try {
            return $timestamp !== null ? new \DateTimeImmutable('@' . $timestamp) : null;
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * @return string
     */
    public function __toString()
    {
        $args = [self::HEADER_ID];
        $format = '0x%04x ExtendedTimestamp:';

        if ($this->modifyTime !== null) {
            $format .= ' Modify:[%s]';
            $args[] = date(\DATE_W3C, $this->modifyTime);
        }

        if ($this->accessTime !== null) {
            $format .= ' Access:[%s]';
            $args[] = date(\DATE_W3C, $this->accessTime);
        }

        if ($this->createTime !== null) {
            $format .= ' Create:[%s]';
            $args[] = date(\DATE_W3C, $this->createTime);
        }

        return vsprintf($format, $args);
    }
}