_get_first_64k_content()   B
last analyzed

Complexity

Conditions 5

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 5
c 1
b 0
f 1
dl 0
loc 20
rs 8.5454
1
"""Implements the public compute function and supporting 'private' functions.
2
"""
3
4
5
from __future__ import absolute_import
6
from __future__ import unicode_literals
7
from datetime import datetime
8
from os import listdir
9
from os.path import (
10
    basename, getctime, getsize, isdir, isfile, join
11
)
12
from struct import pack_into
13
from .crc64calculator import _Crc64Calculator
14
from .exceptions import (
15
    FileContentReadException, FileTimeOutOfRangeException, PathDoesNotExistException
16
)
17
18
19
def compute(dvd_path):
20
    """Computes a Windows API IDvdInfo2::GetDiscID-compatible 64-bit Cyclic Redundancy Check
21
       checksum from the DVD .vob, .ifo and .bup files found in the supplied DVD path.
22
    """
23
24
    _check_dvd_path_exists(dvd_path)
25
26
    _check_video_ts_path_exists(dvd_path)
27
28
    # the polynomial used for this CRC-64 checksum is:
29
    # x^63 + x^60 + x^57 + x^55 + x^54 + x^50 + x^49 + x^46 + x^41 + x^38 + x^37 + x^34 + x^32 +
30
    # x^31 + x^30 + x^28 + x^25 + x^24 + x^21 + x^16 + x^13 + x^12 + x^11 + x^8 + x^7 + x^5 + x^2
31
    calculator = _Crc64Calculator(0x92c64265d32139a4)
32
33
    for video_ts_file_path in _get_video_ts_file_paths(dvd_path):
34
        calculator.update(_get_file_creation_time(video_ts_file_path))
35
        calculator.update(_get_file_size(video_ts_file_path))
36
        calculator.update(_get_file_name(video_ts_file_path))
37
38
    calculator.update(_get_vmgi_file_content(dvd_path))
39
    calculator.update(_get_vts01i_file_content(dvd_path))
40
41
    return calculator.crc64
42
43
44
def _check_dvd_path_exists(dvd_path):
45
    """Raises an exception if the specified DVD path does not exist.
46
    """
47
48
    if not isdir(dvd_path):
49
        raise PathDoesNotExistException(dvd_path)
50
51
52
def _check_video_ts_path_exists(dvd_path):
53
    """Raises an exception if the specified DVD path does not contain a VIDEO_TS folder.
54
    """
55
56
    video_ts_folder_path = join(dvd_path, "VIDEO_TS")
57
58
    if not isdir(video_ts_folder_path):
59
        raise PathDoesNotExistException(video_ts_folder_path)
60
61
62
def _get_video_ts_file_paths(dvd_path):
63
    """Returns a sorted list of paths for files contained in th VIDEO_TS folder of the specified
64
       DVD path.
65
    """
66
67
    video_ts_folder_path = join(dvd_path, "VIDEO_TS")
68
69
    video_ts_file_paths = []
70
71
    for video_ts_folder_content_name in listdir(video_ts_folder_path):
72
        video_ts_folder_content_path = join(video_ts_folder_path, video_ts_folder_content_name)
73
74
        if isfile(video_ts_folder_content_path):
75
            video_ts_file_paths.append(video_ts_folder_content_path)
76
77
    return sorted(video_ts_file_paths)
78
79
80
def _get_file_creation_time(file_path):
81
    """Returns the creation time of the file at the specified file path in Microsoft FILETIME
82
       structure format (https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284.aspx),
83
       formatted as a 8-byte unsigned integer bytearray.
84
    """
85
86
    ctime = getctime(file_path)
87
88
    if ctime < -11644473600 or ctime >= 253402300800:
89
        raise FileTimeOutOfRangeException(ctime)
90
91
    creation_time_datetime = datetime.utcfromtimestamp(ctime)
92
93
    creation_time_epoch_offset = creation_time_datetime - datetime(1601, 1, 1)
94
95
    creation_time_secs_from_epoch = _convert_timedelta_to_seconds(creation_time_epoch_offset)
96
97
    creation_time_filetime = int(creation_time_secs_from_epoch * (10 ** 7))
98
99
    file_creation_time = bytearray(8)
100
    pack_into(b"Q", file_creation_time, 0, creation_time_filetime)
101
102
    return file_creation_time
103
104
105
def _convert_timedelta_to_seconds(timedelta):
106
    """Returns the total seconds calculated from the supplied timedelta.
107
108
       (Function provided to enable running on Python 2.6 which lacks timedelta.total_seconds()).
109
    """
110
111
    days_in_seconds = timedelta.days * 24 * 3600
112
    return int((timedelta.microseconds + (timedelta.seconds + days_in_seconds) * 10 ** 6) / 10 ** 6)
113
114
115
def _get_file_size(file_path):
116
    """Returns the size of the file at the specified file path, formatted as a 4-byte unsigned
117
       integer bytearray.
118
    """
119
120
    size = getsize(file_path)
121
122
    file_size = bytearray(4)
123
    pack_into(b"I", file_size, 0, size)
124
125
    return file_size
126
127
128
def _get_file_name(file_path):
129
    """Returns the name of the file at the specified file path, formatted as a UTF-8 bytearray
130
       terminated with a null character.
131
    """
132
133
    file_name = basename(file_path)
134
135
    utf8_file_name = bytearray(file_name, "utf8")
136
    utf8_file_name.append(0)
137
138
    return utf8_file_name
139
140
141
def _get_vmgi_file_content(dvd_path):
142
    """Returns the first 65536 bytes (or the file size, whichever is smaller) of the VIDEO_TS.IFO
143
       file in the VIDEO_TS folder of the specified DVD path, as a bytearray.
144
    """
145
146
    vmgi_file_path = join(dvd_path, "VIDEO_TS", "VIDEO_TS.IFO")
147
148
    return _get_first_64k_content(vmgi_file_path)
149
150
151
def _get_vts01i_file_content(dvd_path):
152
    """Returns the first 65536 (or the file size, whichever is smaller) bytes of the VTS_01_0.IFO
153
       file in the VIDEO_TS folder of the specified DVD path, as a bytearray.
154
    """
155
156
    vts01i_file_path = join(dvd_path, "VIDEO_TS", "VTS_01_0.IFO")
157
158
    return _get_first_64k_content(vts01i_file_path)
159
160
161
def _get_first_64k_content(file_path):
162
    """Returns the first 65536 (or the file size, whichever is smaller) bytes of the file at the
163
       specified file path, as a bytearray.
164
    """
165
166
    if not isfile(file_path):
167
        raise PathDoesNotExistException(file_path)
168
169
    file_size = getsize(file_path)
170
171
    content_size = min(file_size, 0x10000)
172
173
    content = bytearray(content_size)
174
    with open(file_path, "rb") as file_object:
175
        content_read = file_object.readinto(content)
176
177
        if content_read is None or content_read < content_size:
178
            raise FileContentReadException(content_size, content_read)
179
180
    return content
181