1<?php
2/*
3 * This file is part of the PHPASN1 library.
4 *
5 * Copyright © Friedrich Große <friedrich.grosse@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace FG\ASN1\Universal;
12
13use FG\ASN1\AbstractTime;
14use FG\ASN1\Parsable;
15use FG\ASN1\Identifier;
16use FG\ASN1\Exception\ParserException;
17
18/**
19 * This ASN.1 universal type contains date and time information according to ISO 8601.
20 *
21 * The type consists of values representing:
22 * a) a calendar date, as defined in ISO 8601; and
23 * b) a time of day, to any of the precisions defined in ISO 8601, except for the hours value 24 which shall not be used; and
24 * c) the local time differential factor as defined in ISO 8601.
25 *
26 * Decoding of this type will accept the Basic Encoding Rules (BER)
27 * The encoding will comply with the Distinguished Encoding Rules (DER).
28 */
29class GeneralizedTime extends AbstractTime implements Parsable
30{
31    private $microseconds;
32
33    public function __construct($dateTime = null, $dateTimeZone = 'UTC')
34    {
35        parent::__construct($dateTime, $dateTimeZone);
36        $this->microseconds = $this->value->format('u');
37        if ($this->containsFractionalSecondsElement()) {
38            // DER requires us to remove trailing zeros
39            $this->microseconds = preg_replace('/([1-9]+)0+$/', '$1', $this->microseconds);
40        }
41    }
42
43    public function getType()
44    {
45        return Identifier::GENERALIZED_TIME;
46    }
47
48    protected function calculateContentLength()
49    {
50        $contentSize = 15; // YYYYMMDDHHmmSSZ
51
52        if ($this->containsFractionalSecondsElement()) {
53            $contentSize += 1 + strlen($this->microseconds);
54        }
55
56        return $contentSize;
57    }
58
59    public function containsFractionalSecondsElement()
60    {
61        return intval($this->microseconds) > 0;
62    }
63
64    protected function getEncodedValue()
65    {
66        $encodedContent = $this->value->format('YmdHis');
67        if ($this->containsFractionalSecondsElement()) {
68            $encodedContent .= ".{$this->microseconds}";
69        }
70
71        return $encodedContent.'Z';
72    }
73
74    public function __toString()
75    {
76        if ($this->containsFractionalSecondsElement()) {
77            return $this->value->format("Y-m-d\tH:i:s.uP");
78        } else {
79            return $this->value->format("Y-m-d\tH:i:sP");
80        }
81    }
82
83    public static function fromBinary(&$binaryData, &$offsetIndex = 0)
84    {
85        self::parseIdentifier($binaryData[$offsetIndex], Identifier::GENERALIZED_TIME, $offsetIndex++);
86        $lengthOfMinimumTimeString = 14; // YYYYMMDDHHmmSS
87        $contentLength = self::parseContentLength($binaryData, $offsetIndex, $lengthOfMinimumTimeString);
88        $maximumBytesToRead = $contentLength;
89
90        $format = 'YmdGis';
91        $content = substr($binaryData, $offsetIndex, $contentLength);
92        $dateTimeString = substr($content, 0, $lengthOfMinimumTimeString);
93        $offsetIndex += $lengthOfMinimumTimeString;
94        $maximumBytesToRead -= $lengthOfMinimumTimeString;
95
96        if ($contentLength == $lengthOfMinimumTimeString) {
97            $localTimeZone = new \DateTimeZone(date_default_timezone_get());
98            $dateTime = \DateTime::createFromFormat($format, $dateTimeString, $localTimeZone);
99        } else {
100            if ($binaryData[$offsetIndex] == '.') {
101                $maximumBytesToRead--; // account for the '.'
102                $nrOfFractionalSecondElements = 1; // account for the '.'
103
104                while ($maximumBytesToRead > 0
105                      && $binaryData[$offsetIndex + $nrOfFractionalSecondElements] != '+'
106                      && $binaryData[$offsetIndex + $nrOfFractionalSecondElements] != '-'
107                      && $binaryData[$offsetIndex + $nrOfFractionalSecondElements] != 'Z') {
108                    $nrOfFractionalSecondElements++;
109                    $maximumBytesToRead--;
110                }
111
112                $dateTimeString .= substr($binaryData, $offsetIndex, $nrOfFractionalSecondElements);
113                $offsetIndex += $nrOfFractionalSecondElements;
114                $format .= '.u';
115            }
116
117            $dateTime = \DateTime::createFromFormat($format, $dateTimeString, new \DateTimeZone('UTC'));
118
119            if ($maximumBytesToRead > 0) {
120                if ($binaryData[$offsetIndex] == '+'
121                || $binaryData[$offsetIndex] == '-') {
122                    $dateTime = static::extractTimeZoneData($binaryData, $offsetIndex, $dateTime);
123                } elseif ($binaryData[$offsetIndex++] != 'Z') {
124                    throw new ParserException('Invalid ISO 8601 Time String', $offsetIndex);
125                }
126            }
127        }
128
129        $parsedObject = new self($dateTime);
130        $parsedObject->setContentLength($contentLength);
131
132        return $parsedObject;
133    }
134}
135