File: dates.py

package info (click to toggle)
python-pycdlib 1.12.0%2Bds1-7
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 3,748 kB
  • sloc: python: 36,118; makefile: 63
file content (268 lines) | stat: -rw-r--r-- 10,380 bytes parent folder | download | duplicates (2)
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
# Copyright (C) 2015-2019  Chris Lalancette <clalancette@gmail.com>

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.

# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

'''
Classes and utilities for ISO date support.
'''

from __future__ import absolute_import

import struct
import time
try:
    from functools import lru_cache
except ImportError:
    from pycdlib.backport_functools import lru_cache  # type: ignore

from pycdlib import pycdlibexception
from pycdlib import utils


@lru_cache(maxsize=256)
def string_to_timestruct(input_string):
    # type: (bytes) -> time.struct_time
    '''
    A cacheable function to take an input string and decode it into a
    time.struct_time from the time module.  If the string cannot be decoded
    because of an illegal value, then the all-zero time.struct_time will be
    returned instead.

    Parameters:
     input_string - The string to attempt to parse.
    Returns:
     A time.struct_time object representing the time.
    '''
    try:
        timestruct = time.strptime(input_string.decode('utf-8'), VolumeDescriptorDate.TIME_FMT)
    except ValueError:
        # Ecma-119, 8.4.26.1 specifies that if the string was all the digit
        # zero, with the last byte 0, the time wasn't specified.  In that
        # case, time.strptime() with our format will raise a ValueError.
        # In practice we have found that some ISOs specify various wacky
        # things in this field, so if we see *any* ValueError, we just
        # assume the date is unspecified and go with that.
        timestruct = time.struct_time((0, 0, 0, 0, 0, 0, 0, 0, 0))

    return timestruct


class DirectoryRecordDate(object):
    '''
    A class to represent a Directory Record date as described in Ecma-119
    section 9.1.5.  The Directory Record date consists of the number of years
    since 1900, the month, the day of the month, the hour, the minute, the
    second, and the offset from GMT in 15 minute intervals.  There are two main
    ways to use this class: either to instantiate and then parse a string to
    fill in the fields (the parse() method), or to create a new entry with a
    tm structure (the new() method).
    '''
    FMT = '=BBBBBBb'

    __slots__ = ('_initialized', 'years_since_1900', 'month', 'day_of_month',
                 'hour', 'minute', 'second', 'gmtoffset')

    def __init__(self):
        # type: () -> None
        self._initialized = False

    def parse(self, datestr):
        # type: (bytes) -> None
        '''
        Parse a Directory Record date out of a string.

        Parameters:
         datestr - The string to parse the date out of.
        Returns:
         Nothing.
        '''
        if self._initialized:
            raise pycdlibexception.PyCdlibInternalError('Directory Record Date already initialized')

        (self.years_since_1900, self.month, self.day_of_month, self.hour,
         self.minute, self.second,
         self.gmtoffset) = struct.unpack_from(self.FMT, datestr, 0)

        self._initialized = True

    def new(self):
        # type: () -> None
        '''
        Create a new Directory Record date based on the current time.

        Parameters:
         tm - An optional argument that must be None
        Returns:
         Nothing.
        '''
        if self._initialized:
            raise pycdlibexception.PyCdlibInternalError('Directory Record Date already initialized')

        # This algorithm was ported from cdrkit, genisoimage.c:iso9660_date()
        tm = time.time()
        local = time.localtime(tm)
        self.years_since_1900 = local.tm_year - 1900
        self.month = local.tm_mon
        self.day_of_month = local.tm_mday
        self.hour = local.tm_hour
        self.minute = local.tm_min
        self.second = local.tm_sec
        self.gmtoffset = utils.gmtoffset_from_tm(tm, local)
        self._initialized = True

    def record(self):
        # type: () -> bytes
        '''
        Return a string representation of the Directory Record date.

        Parameters:
         None.
        Returns:
         A string representing this Directory Record Date.
        '''
        if not self._initialized:
            raise pycdlibexception.PyCdlibInternalError('Directory Record Date not initialized')

        return struct.pack(self.FMT, self.years_since_1900, self.month,
                           self.day_of_month, self.hour, self.minute,
                           self.second, self.gmtoffset)

    def __ne__(self, other):
        return self.years_since_1900 != other.years_since_1900 or self.month != other.month or self.day_of_month != other.day_of_month or self.hour != other.hour or self.minute != other.minute or self.second != other.second or self.gmtoffset != other.gmtoffset


class VolumeDescriptorDate(object):
    '''
    A class to represent a Volume Descriptor Date as described in Ecma-119
    section 8.4.26.1.  The Volume Descriptor Date consists of a year (from 1 to
    9999), month (from 1 to 12), day of month (from 1 to 31), hour (from 0
    to 23), minute (from 0 to 59), second (from 0 to 59), hundredths of second,
    and offset from GMT in 15-minute intervals (from -48 to +52) fields.  There
    are two main ways to use this class: either to instantiate and then parse a
    string to fill in the fields (the parse() method), or to create a new entry
    with a tm structure (the new() method).
    '''

    TIME_FMT = '%Y%m%d%H%M%S'

    EMPTY_STRING = b'0' * 16 + b'\x00'

    __slots__ = ('_initialized', 'year', 'month', 'dayofmonth', 'hour',
                 'minute', 'second', 'hundredthsofsecond', 'gmtoffset',
                 'date_str')

    def __init__(self):
        # type: () -> None
        self._initialized = False

    def parse(self, datestr):
        # type: (bytes) -> None
        '''
        Parse a Volume Descriptor Date out of a string.  A string of all zeros
        is valid, which means that the date in this field was not specified.

        Parameters:
          datestr - string to be parsed
        Returns:
          Nothing.
        '''
        if self._initialized:
            raise pycdlibexception.PyCdlibInternalError('This Volume Descriptor Date object is already initialized')

        if len(datestr) != 17:
            raise pycdlibexception.PyCdlibInvalidISO('Invalid ISO9660 date string')

        timestruct = string_to_timestruct(datestr[:-3])
        self.year = timestruct.tm_year
        self.month = timestruct.tm_mon
        self.dayofmonth = timestruct.tm_mday
        self.hour = timestruct.tm_hour
        self.minute = timestruct.tm_min
        self.second = timestruct.tm_sec
        if timestruct.tm_year == 0 and timestruct.tm_mon == 0 and timestruct.tm_mday == 0 and timestruct.tm_hour == 0 and timestruct.tm_min == 0 and timestruct.tm_sec == 0:
            self.hundredthsofsecond = 0
            self.gmtoffset = 0
            self.date_str = self.EMPTY_STRING
        else:
            try:
                self.hundredthsofsecond = int(datestr[14:16])
            except ValueError:
                # We've seen ISOs in the wild (made by MagicISO) that fill
                # hundredthsofseconds with b'\x00\x00'.  Handle that here.
                self.hundredthsofsecond, = struct.unpack('>H', datestr[14:16])
            self.gmtoffset, = struct.unpack_from('=b', datestr, 16)
            self.date_str = datestr

        self._initialized = True

    def record(self):
        # type: () -> bytes
        '''
        Return the date string for this object.

        Parameters:
          None.
        Returns:
          Date as a string.
        '''
        if not self._initialized:
            raise pycdlibexception.PyCdlibInternalError('This Volume Descriptor Date is not initialized')

        return self.date_str

    def new(self, tm=0.0):
        # type: (float) -> None
        '''
        Create a new Volume Descriptor Date.  If tm is None, then this Volume
        Descriptor Date will be full of zeros (meaning not specified).  If tm
        is not None, it is expected to be a struct_time object, at which point
        this Volume Descriptor Date object will be filled in with data from that
        struct_time.

        Parameters:
          tm - struct_time object to base new VolumeDescriptorDate off of,
               or 0.0 for an empty VolumeDescriptorDate.
        Returns:
          Nothing.
        '''
        if self._initialized:
            raise pycdlibexception.PyCdlibInternalError('This Volume Descriptor Date object is already initialized')

        if tm != 0.0:
            local = time.localtime(tm)
            self.year = local.tm_year
            self.month = local.tm_mon
            self.dayofmonth = local.tm_mday
            self.hour = local.tm_hour
            self.minute = local.tm_min
            self.second = local.tm_sec
            self.hundredthsofsecond = 0
            self.gmtoffset = utils.gmtoffset_from_tm(tm, local)
            self.date_str = time.strftime(self.TIME_FMT, local).encode('utf-8') + '{:0<2}'.format(self.hundredthsofsecond).encode('utf-8') + struct.pack('=b', self.gmtoffset)
        else:
            self.year = 0
            self.month = 0
            self.dayofmonth = 0
            self.hour = 0
            self.minute = 0
            self.second = 0
            self.hundredthsofsecond = 0
            self.gmtoffset = 0
            self.date_str = self.EMPTY_STRING

        self._initialized = True

    def __ne__(self, other):
        return self.year != other.year or self.month != other.month or self.dayofmonth != other.dayofmonth or self.hour != other.hour or self.minute != other.minute or self.second != other.second or self.hundredthsofsecond != other.hundredthsofsecond or self.gmtoffset != other.gmtoffset or self.date_str != other.date_str