udfkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- udfkit/__init__.py +12 -0
- udfkit/__main__.py +41 -0
- udfkit/bluray.py +492 -0
- udfkit/dvd.py +1086 -0
- udfkit/errors.py +11 -0
- udfkit/info.py +51 -0
- udfkit-0.1.0.dist-info/METADATA +112 -0
- udfkit-0.1.0.dist-info/RECORD +13 -0
- udfkit-0.1.0.dist-info/WHEEL +5 -0
- udfkit-0.1.0.dist-info/entry_points.txt +2 -0
- udfkit-0.1.0.dist-info/licenses/LICENSE +202 -0
- udfkit-0.1.0.dist-info/licenses/NOTICE +5 -0
- udfkit-0.1.0.dist-info/top_level.txt +1 -0
udfkit/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""udfkit: write DVD-Video and Blu-ray disc images in pure Python.
|
|
2
|
+
|
|
3
|
+
* `udfkit.dvd.write_image(src, out, label)`: a DVD-Video image (UDF 1.02 +
|
|
4
|
+
ISO 9660 "bridge"), its VIDEO_TS files placed where their IFO files say.
|
|
5
|
+
* `udfkit.bluray.write_image(src, out, label)`: a Blu-ray image (UDF 2.50
|
|
6
|
+
with a metadata partition), as BD-ROM players and libbluray read it.
|
|
7
|
+
* `udfkit.info.volume_names(image)`: the volume names an image carries.
|
|
8
|
+
|
|
9
|
+
No external programs and no dependencies beyond the standard library.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
udfkit/__main__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Command line: `udfkit dvd|bluray SRC IMAGE --label NAME`, `udfkit info IMAGE`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, bluray, dvd, info
|
|
8
|
+
from .errors import UdfError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv=None):
|
|
12
|
+
p = argparse.ArgumentParser(prog="udfkit", description="Write DVD-Video and Blu-ray disc "
|
|
13
|
+
"images (UDF) without external tools.")
|
|
14
|
+
p.add_argument("--version", action="version", version=f"udfkit {__version__}")
|
|
15
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
16
|
+
for name, what in (("dvd", "a folder with VIDEO_TS (and optional AUDIO_TS, DVD-ROM files)"),
|
|
17
|
+
("bluray", "a folder with BDMV (and optional CERTIFICATE)")):
|
|
18
|
+
s = sub.add_parser(name, help=f"write an image of {what}")
|
|
19
|
+
s.add_argument("src", help=what)
|
|
20
|
+
s.add_argument("image", help="the image file to write")
|
|
21
|
+
s.add_argument("--label", required=True, help="volume label (A-Z, 0-9, _; 1-32)")
|
|
22
|
+
s = sub.add_parser("info", help="the volume names an image carries")
|
|
23
|
+
s.add_argument("image")
|
|
24
|
+
args = p.parse_args(argv)
|
|
25
|
+
try:
|
|
26
|
+
if args.command == "info":
|
|
27
|
+
print(json.dumps(info.volume_names(args.image), indent=2, ensure_ascii=False))
|
|
28
|
+
return 0
|
|
29
|
+
writer = dvd if args.command == "dvd" else bluray
|
|
30
|
+
stats = writer.write_image(args.src, args.image, args.label)
|
|
31
|
+
except UdfError as e:
|
|
32
|
+
print(f"udfkit: {e.message}", file=sys.stderr)
|
|
33
|
+
if e.hint:
|
|
34
|
+
print(f" {e.hint}", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
print(f"{stats['path']}: {stats['bytes']} bytes, {stats['files']} files")
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
sys.exit(main())
|
udfkit/bluray.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Write a Blu-ray disc image (UDF 2.50) in pure Python.
|
|
2
|
+
|
|
3
|
+
Blu-ray discs use UDF 2.50 only, no ISO 9660. What sets 2.50 apart from the
|
|
4
|
+
UDF 1.02 of DVD-Video (udfwriter.py) is the metadata partition: the file
|
|
5
|
+
system's own structures (file set, directories, file entries) live in a
|
|
6
|
+
"metadata file" inside the physical partition, recorded twice (the main
|
|
7
|
+
file and its mirror) so one damaged copy does not lose the directory tree.
|
|
8
|
+
Readers find it through a second (type 2) partition map.
|
|
9
|
+
|
|
10
|
+
sector contents
|
|
11
|
+
0-15 zeros
|
|
12
|
+
16-18 Volume Recognition Sequence: BEA01, NSR03, TEA01
|
|
13
|
+
32-47 Main Volume Descriptor Sequence: Primary VD, Implementation Use
|
|
14
|
+
VD ("*UDF LV Info"), Partition D, Logical Volume D (a type 1
|
|
15
|
+
map and a metadata partition map), Unallocated Space D,
|
|
16
|
+
Terminating D
|
|
17
|
+
48-63 Reserve Volume Descriptor Sequence (the same)
|
|
18
|
+
64-65 Logical Volume Integrity Sequence: LVID, Terminating D
|
|
19
|
+
256 Anchor Volume Descriptor Pointer
|
|
20
|
+
P = 257 partition block 0: Extended File Entry of the metadata file
|
|
21
|
+
partition block 1: Extended File Entry of the metadata mirror
|
|
22
|
+
partition block 32: the metadata file (M blocks, 32-block units):
|
|
23
|
+
block 0, 1 File Set Descriptor, Terminating D
|
|
24
|
+
then each directory: File Entry + File Identifier
|
|
25
|
+
Descriptors (root first, pre-order)
|
|
26
|
+
then one File Entry per file
|
|
27
|
+
then the file data, each file contiguous, in tree order
|
|
28
|
+
then the metadata mirror (the same M blocks, 32-block aligned)
|
|
29
|
+
N-1 Anchor Volume Descriptor Pointer again
|
|
30
|
+
|
|
31
|
+
Locations inside the metadata file are metadata-partition blocks (block 0
|
|
32
|
+
is the file set); File Entries point at the data with long_ad descriptors
|
|
33
|
+
naming the physical partition.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import datetime
|
|
37
|
+
import os
|
|
38
|
+
import struct
|
|
39
|
+
import zlib
|
|
40
|
+
|
|
41
|
+
from .errors import UdfError
|
|
42
|
+
from .dvd import (ACCESS_READ_ONLY, DOMAIN_FLAGS, DOMAIN_ID, FID_DIRECTORY,
|
|
43
|
+
FID_PARENT, FIRST_UNIQUE_ID, ICB_FLAGS, ICB_STRATEGY,
|
|
44
|
+
IMPLEMENTATION_ID, INTEGRITY_CLOSE, LV_INFO_ID, MAX_EXTENT,
|
|
45
|
+
PARTITION_FLAG_ALLOCATED, PERM_DIRECTORY, PERM_FILE, SECTOR,
|
|
46
|
+
TAG_ANCHOR_POINTER, TAG_FILE_ENTRY, TAG_FILE_IDENTIFIER,
|
|
47
|
+
TAG_FILE_SET, TAG_IMPLEMENTATION_USE, TAG_LOGICAL_VOLUME,
|
|
48
|
+
TAG_LOGICAL_VOLUME_INTEGRITY, TAG_PARTITION, TAG_PRIMARY_VOLUME,
|
|
49
|
+
TAG_TERMINATING, TAG_UNALLOCATED_SPACE, UID_NONE, charspec, cs0,
|
|
50
|
+
crc_itu_t, dstring, entity_id, tag_checksum, udf_timestamp,
|
|
51
|
+
validate_label, volume_recognition, _CHUNK, _sectors)
|
|
52
|
+
|
|
53
|
+
UDF_REVISION = 0x0250
|
|
54
|
+
DESCRIPTOR_VERSION = 3 # NSR03
|
|
55
|
+
PARTITION_CONTENTS_ID = b"+NSR03"
|
|
56
|
+
METADATA_PARTITION_ID = b"*UDF Metadata Partition"
|
|
57
|
+
TAG_EXTENDED_FILE_ENTRY = 266
|
|
58
|
+
FILE_TYPE_DIRECTORY = 4
|
|
59
|
+
FILE_TYPE_FILE = 5
|
|
60
|
+
FILE_TYPE_METADATA = 250
|
|
61
|
+
FILE_TYPE_METADATA_MIRROR = 251
|
|
62
|
+
AD_SHORT, AD_LONG = 0, 1 # ICB flags bits 0-2 (ECMA-167 4/14.6.8)
|
|
63
|
+
|
|
64
|
+
VRS_SECTOR = 16
|
|
65
|
+
MAIN_VDS_SECTOR = 32
|
|
66
|
+
RESERVE_VDS_SECTOR = 48
|
|
67
|
+
VDS_SECTORS = 16
|
|
68
|
+
LVIS_SECTOR = 64
|
|
69
|
+
LVIS_SECTORS = 2
|
|
70
|
+
ANCHOR_SECTOR = 256
|
|
71
|
+
PARTITION_START = 257
|
|
72
|
+
META_UNIT = 32 # allocation and alignment unit of the metadata file
|
|
73
|
+
META_START = 32 # partition block where the metadata file begins
|
|
74
|
+
PHYSICAL, METADATA = 0, 1 # partition reference numbers (map order)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _tagged(buf, tag_id, location):
|
|
78
|
+
struct.pack_into("<HHBBHHHI", buf, 0, tag_id, DESCRIPTOR_VERSION, 0, 0, 0,
|
|
79
|
+
crc_itu_t(buf[16:]), len(buf) - 16, location)
|
|
80
|
+
buf[4] = tag_checksum(buf)
|
|
81
|
+
return bytes(buf)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _domain_id():
|
|
85
|
+
return entity_id(DOMAIN_ID, struct.pack("<HB", UDF_REVISION, DOMAIN_FLAGS))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _impl_id():
|
|
89
|
+
return entity_id(IMPLEMENTATION_ID)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _align(n, unit):
|
|
93
|
+
return -(-n // unit) * unit
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ==============================================================================
|
|
97
|
+
# volume structures
|
|
98
|
+
# ==============================================================================
|
|
99
|
+
|
|
100
|
+
def anchor_pointer(location):
|
|
101
|
+
buf = bytearray(512)
|
|
102
|
+
struct.pack_into("<IIII", buf, 16, VDS_SECTORS * SECTOR, MAIN_VDS_SECTOR,
|
|
103
|
+
VDS_SECTORS * SECTOR, RESERVE_VDS_SECTOR)
|
|
104
|
+
return _tagged(buf, TAG_ANCHOR_POINTER, location)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def primary_volume(location, label, volume_set, date):
|
|
108
|
+
buf = bytearray(512)
|
|
109
|
+
buf[24:56] = dstring(label, 32)
|
|
110
|
+
struct.pack_into("<HHHHII", buf, 56, 1, 1, 2, 2, 1, 1)
|
|
111
|
+
buf[72:200] = dstring(volume_set, 128)
|
|
112
|
+
buf[200:264] = charspec()
|
|
113
|
+
buf[264:328] = charspec()
|
|
114
|
+
buf[376:388] = udf_timestamp(date)
|
|
115
|
+
buf[388:420] = _impl_id()
|
|
116
|
+
return _tagged(buf, TAG_PRIMARY_VOLUME, location)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def implementation_use_volume(location, label):
|
|
120
|
+
buf = bytearray(512)
|
|
121
|
+
struct.pack_into("<I", buf, 16, 1)
|
|
122
|
+
buf[20:52] = entity_id(LV_INFO_ID, struct.pack("<H", UDF_REVISION))
|
|
123
|
+
buf[52:116] = charspec()
|
|
124
|
+
buf[116:244] = dstring(label, 128)
|
|
125
|
+
buf[352:384] = _impl_id()
|
|
126
|
+
return _tagged(buf, TAG_IMPLEMENTATION_USE, location)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def partition(location, start, length):
|
|
130
|
+
buf = bytearray(512)
|
|
131
|
+
struct.pack_into("<IHH", buf, 16, 2, PARTITION_FLAG_ALLOCATED, 0)
|
|
132
|
+
buf[24:56] = entity_id(PARTITION_CONTENTS_ID)
|
|
133
|
+
struct.pack_into("<III", buf, 184, ACCESS_READ_ONLY, start, length)
|
|
134
|
+
buf[196:228] = _impl_id()
|
|
135
|
+
return _tagged(buf, TAG_PARTITION, location)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def metadata_partition_map(meta_block, mirror_block):
|
|
139
|
+
"""UDF 2.50 2.2.10 Metadata Partition Map (a type 2 map, 64 bytes)."""
|
|
140
|
+
m = bytearray(64)
|
|
141
|
+
m[0], m[1] = 2, 64
|
|
142
|
+
m[4:36] = entity_id(METADATA_PARTITION_ID, struct.pack("<H", UDF_REVISION))
|
|
143
|
+
struct.pack_into("<HHIIIIHB", m, 36, 1, 0, meta_block, mirror_block, 0xFFFFFFFF,
|
|
144
|
+
META_UNIT, META_UNIT, 1) # no bitmap; the mirror duplicates
|
|
145
|
+
return bytes(m)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def logical_volume(location, label, meta_block, mirror_block):
|
|
149
|
+
maps = struct.pack("<BBHH", 1, 6, 1, 0) + metadata_partition_map(meta_block, mirror_block)
|
|
150
|
+
buf = bytearray(440 + len(maps))
|
|
151
|
+
struct.pack_into("<I", buf, 16, 3)
|
|
152
|
+
buf[20:84] = charspec()
|
|
153
|
+
buf[84:212] = dstring(label, 128)
|
|
154
|
+
struct.pack_into("<I", buf, 212, SECTOR)
|
|
155
|
+
buf[216:248] = _domain_id()
|
|
156
|
+
# the File Set Descriptor: metadata partition, block 0
|
|
157
|
+
struct.pack_into("<IIH", buf, 248, 2 * SECTOR, 0, METADATA)
|
|
158
|
+
struct.pack_into("<II", buf, 264, len(maps), 2)
|
|
159
|
+
buf[272:304] = _impl_id()
|
|
160
|
+
struct.pack_into("<II", buf, 432, LVIS_SECTORS * SECTOR, LVIS_SECTOR)
|
|
161
|
+
buf[440:] = maps
|
|
162
|
+
return _tagged(buf, TAG_LOGICAL_VOLUME, location)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def unallocated_space(location):
|
|
166
|
+
buf = bytearray(24)
|
|
167
|
+
struct.pack_into("<II", buf, 16, 4, 0)
|
|
168
|
+
return _tagged(buf, TAG_UNALLOCATED_SPACE, location)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def terminating(location):
|
|
172
|
+
return _tagged(bytearray(512), TAG_TERMINATING, location)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def logical_volume_integrity(location, date, next_unique_id, sizes, files, directories):
|
|
176
|
+
"""Closed LVID; `sizes` is the size of each partition (physical, metadata)."""
|
|
177
|
+
n = len(sizes)
|
|
178
|
+
buf = bytearray(80 + 8 * n + 46)
|
|
179
|
+
buf[16:28] = udf_timestamp(date)
|
|
180
|
+
struct.pack_into("<I", buf, 28, INTEGRITY_CLOSE)
|
|
181
|
+
struct.pack_into("<Q", buf, 40, next_unique_id)
|
|
182
|
+
struct.pack_into("<II", buf, 72, n, 46)
|
|
183
|
+
for i, size in enumerate(sizes):
|
|
184
|
+
struct.pack_into("<II", buf, 80 + 4 * i, 0, 0) # free space: none
|
|
185
|
+
struct.pack_into("<I", buf, 80 + 4 * n + 4 * i, size)
|
|
186
|
+
imp = 80 + 8 * n
|
|
187
|
+
buf[imp:imp + 32] = _impl_id()
|
|
188
|
+
struct.pack_into("<IIHHH", buf, imp + 32, files, directories,
|
|
189
|
+
UDF_REVISION, UDF_REVISION, UDF_REVISION)
|
|
190
|
+
return _tagged(buf, TAG_LOGICAL_VOLUME_INTEGRITY, location)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ==============================================================================
|
|
194
|
+
# file structures
|
|
195
|
+
# ==============================================================================
|
|
196
|
+
|
|
197
|
+
def file_set(label, root_block, date):
|
|
198
|
+
buf = bytearray(512)
|
|
199
|
+
buf[16:28] = udf_timestamp(date)
|
|
200
|
+
struct.pack_into("<HHIIII", buf, 28, 3, 3, 1, 1, 0, 0)
|
|
201
|
+
buf[48:112] = charspec()
|
|
202
|
+
buf[112:240] = dstring(label, 128)
|
|
203
|
+
buf[240:304] = charspec()
|
|
204
|
+
buf[304:336] = dstring(label, 32)
|
|
205
|
+
struct.pack_into("<IIH", buf, 400, SECTOR, root_block, METADATA)
|
|
206
|
+
buf[416:448] = _domain_id()
|
|
207
|
+
return _tagged(buf, TAG_FILE_SET, 0)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def file_identifier(block, name, characteristics, icb_block, unique_id):
|
|
211
|
+
ident = cs0(name) if name is not None else b""
|
|
212
|
+
if len(ident) > 255:
|
|
213
|
+
raise UdfError(f"file name too long for UDF: {name!r}")
|
|
214
|
+
buf = bytearray((38 + len(ident) + 3) & ~3)
|
|
215
|
+
struct.pack_into("<HBB", buf, 16, 1, characteristics, len(ident))
|
|
216
|
+
struct.pack_into("<IIHHIH", buf, 20, SECTOR, icb_block, METADATA, 0,
|
|
217
|
+
unique_id & 0xFFFFFFFF, 0)
|
|
218
|
+
buf[38:38 + len(ident)] = ident
|
|
219
|
+
return _tagged(buf, TAG_FILE_IDENTIFIER, block)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _extents(size, block):
|
|
223
|
+
out, remaining = [], size
|
|
224
|
+
while remaining > 0:
|
|
225
|
+
chunk = min(remaining, MAX_EXTENT)
|
|
226
|
+
out.append((chunk, block))
|
|
227
|
+
block += chunk // SECTOR
|
|
228
|
+
remaining -= chunk
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _icb_tag(buf, file_type, ad_type):
|
|
233
|
+
struct.pack_into("<IHHHBB", buf, 16, 0, ICB_STRATEGY, 0, 1, 0, file_type)
|
|
234
|
+
struct.pack_into("<H", buf, 34, ICB_FLAGS | ad_type)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def file_entry(block, is_dir, size, data_block, unique_id, link_count, date):
|
|
238
|
+
"""File Entry in the metadata partition. A directory's identifiers are
|
|
239
|
+
in the metadata partition too (short_ad); a file's data is in the
|
|
240
|
+
physical partition (long_ad)."""
|
|
241
|
+
if is_dir:
|
|
242
|
+
ads = b"".join(struct.pack("<II", n, b) for n, b in _extents(size, data_block))
|
|
243
|
+
else:
|
|
244
|
+
ads = b"".join(struct.pack("<IIH6x", n, b, PHYSICAL)
|
|
245
|
+
for n, b in _extents(size, data_block))
|
|
246
|
+
if 176 + len(ads) > SECTOR:
|
|
247
|
+
raise UdfError("file too large for one UDF File Entry")
|
|
248
|
+
buf = bytearray(176 + len(ads))
|
|
249
|
+
_icb_tag(buf, FILE_TYPE_DIRECTORY if is_dir else FILE_TYPE_FILE,
|
|
250
|
+
AD_SHORT if is_dir else AD_LONG)
|
|
251
|
+
struct.pack_into("<IIIHBBI", buf, 36, UID_NONE, UID_NONE,
|
|
252
|
+
PERM_DIRECTORY if is_dir else PERM_FILE, link_count, 0, 0, 0)
|
|
253
|
+
struct.pack_into("<QQ", buf, 56, size, _sectors(size))
|
|
254
|
+
stamp = udf_timestamp(date)
|
|
255
|
+
buf[72:84] = buf[84:96] = buf[96:108] = stamp
|
|
256
|
+
struct.pack_into("<I", buf, 108, 1)
|
|
257
|
+
buf[128:160] = _impl_id()
|
|
258
|
+
struct.pack_into("<QII", buf, 160, unique_id, 0, len(ads))
|
|
259
|
+
buf[176:] = ads
|
|
260
|
+
return _tagged(buf, TAG_FILE_ENTRY, block)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def metadata_file_entry(block, file_type, start, blocks, date):
|
|
264
|
+
"""ECMA-167 4/14.17 Extended File Entry of the metadata file or its
|
|
265
|
+
mirror: one short_ad extent in the physical partition."""
|
|
266
|
+
ads = struct.pack("<II", blocks * SECTOR, start)
|
|
267
|
+
buf = bytearray(216 + len(ads))
|
|
268
|
+
_icb_tag(buf, file_type, AD_SHORT)
|
|
269
|
+
struct.pack_into("<IIIHBBI", buf, 36, UID_NONE, UID_NONE, 0, 1, 0, 0, 0)
|
|
270
|
+
struct.pack_into("<QQQ", buf, 56, blocks * SECTOR, blocks * SECTOR, blocks)
|
|
271
|
+
stamp = udf_timestamp(date)
|
|
272
|
+
buf[80:92] = buf[92:104] = buf[104:116] = buf[116:128] = stamp
|
|
273
|
+
struct.pack_into("<I", buf, 128, 1)
|
|
274
|
+
buf[168:200] = _impl_id()
|
|
275
|
+
struct.pack_into("<QII", buf, 200, 0, 0, len(ads))
|
|
276
|
+
buf[216:] = ads
|
|
277
|
+
return _tagged(buf, TAG_EXTENDED_FILE_ENTRY, block)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ==============================================================================
|
|
281
|
+
# the tree
|
|
282
|
+
# ==============================================================================
|
|
283
|
+
|
|
284
|
+
class _Node:
|
|
285
|
+
__slots__ = ("name", "src", "is_dir", "size", "children", "parent", "fe_block",
|
|
286
|
+
"dir_bytes", "unique_id", "block")
|
|
287
|
+
|
|
288
|
+
def __init__(self, name, src, is_dir, size=0, parent=None):
|
|
289
|
+
self.name, self.src, self.is_dir, self.size, self.parent = name, src, is_dir, size, parent
|
|
290
|
+
self.children = []
|
|
291
|
+
self.fe_block = self.dir_bytes = self.unique_id = self.block = None
|
|
292
|
+
|
|
293
|
+
def path(self):
|
|
294
|
+
parts, n = [], self
|
|
295
|
+
while n.parent is not None:
|
|
296
|
+
parts.append(n.name)
|
|
297
|
+
n = n.parent
|
|
298
|
+
return "/" + "/".join(reversed(parts))
|
|
299
|
+
|
|
300
|
+
def walk(self):
|
|
301
|
+
yield self
|
|
302
|
+
for c in self.children:
|
|
303
|
+
yield from c.walk()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def scan_tree(src_dir, skip=None):
|
|
307
|
+
"""Everything under src_dir except hidden entries, by name."""
|
|
308
|
+
root = _Node("", src_dir, True)
|
|
309
|
+
|
|
310
|
+
def scan(node, path):
|
|
311
|
+
try:
|
|
312
|
+
entries = sorted(os.scandir(path), key=lambda e: e.name)
|
|
313
|
+
except OSError as e:
|
|
314
|
+
raise UdfError(f"cannot read {path}: {e.strerror}") from e
|
|
315
|
+
for e in entries:
|
|
316
|
+
if e.name.startswith(".") or os.path.realpath(e.path) == skip:
|
|
317
|
+
continue
|
|
318
|
+
if e.is_dir():
|
|
319
|
+
child = _Node(e.name, e.path, True, parent=node)
|
|
320
|
+
scan(child, e.path)
|
|
321
|
+
elif e.is_file():
|
|
322
|
+
child = _Node(e.name, e.path, False, e.stat().st_size, parent=node)
|
|
323
|
+
else:
|
|
324
|
+
continue
|
|
325
|
+
node.children.append(child)
|
|
326
|
+
scan(root, src_dir)
|
|
327
|
+
return root
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# ==============================================================================
|
|
331
|
+
# writing
|
|
332
|
+
# ==============================================================================
|
|
333
|
+
|
|
334
|
+
def write_image(src_dir, out_path, label, *, date=None, on_progress=None):
|
|
335
|
+
"""Write a UDF 2.50 image of `src_dir` (a BDMV disc folder) to out_path.
|
|
336
|
+
Returns statistics like udfkit.dvd.write_image."""
|
|
337
|
+
validate_label(label)
|
|
338
|
+
if date is None:
|
|
339
|
+
date = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
|
|
340
|
+
out_path = os.path.abspath(out_path)
|
|
341
|
+
root = scan_tree(src_dir, skip=os.path.realpath(out_path))
|
|
342
|
+
if not any(c.name == "BDMV" and c.is_dir for c in root.children):
|
|
343
|
+
raise UdfError(f"{src_dir} has no BDMV directory")
|
|
344
|
+
layout = _plan(root)
|
|
345
|
+
try:
|
|
346
|
+
_write(root, layout, out_path, label, date, on_progress)
|
|
347
|
+
except BaseException:
|
|
348
|
+
try:
|
|
349
|
+
os.remove(out_path)
|
|
350
|
+
except OSError:
|
|
351
|
+
pass
|
|
352
|
+
raise
|
|
353
|
+
files = [n for n in root.walk() if not n.is_dir]
|
|
354
|
+
return {"path": out_path, "sectors": layout["total"], "bytes": layout["total"] * SECTOR,
|
|
355
|
+
"files": len(files), "directories": len(layout["dirs"]), "label": label,
|
|
356
|
+
"udf_revision": "2.50",
|
|
357
|
+
"extents": {n.path(): (PARTITION_START + n.block, n.size) for n in files}}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _plan(root):
|
|
361
|
+
dirs = [n for n in root.walk() if n.is_dir]
|
|
362
|
+
files = [n for n in root.walk() if not n.is_dir]
|
|
363
|
+
block, uid = 2, FIRST_UNIQUE_ID # after the file set and its terminator
|
|
364
|
+
for d in dirs:
|
|
365
|
+
if d is root:
|
|
366
|
+
d.unique_id = 0
|
|
367
|
+
else:
|
|
368
|
+
d.unique_id, uid = uid, uid + 1
|
|
369
|
+
d.dir_bytes = len(file_identifier(0, None, 0, 0, 0)) + sum(
|
|
370
|
+
len(file_identifier(0, c.name, 0, 0, 0)) for c in d.children)
|
|
371
|
+
d.fe_block = block
|
|
372
|
+
block += 1 + _sectors(d.dir_bytes)
|
|
373
|
+
for f in files:
|
|
374
|
+
f.unique_id, uid = uid, uid + 1
|
|
375
|
+
f.fe_block = block
|
|
376
|
+
block += 1
|
|
377
|
+
meta_blocks = _align(block, META_UNIT)
|
|
378
|
+
pos = META_START + meta_blocks
|
|
379
|
+
for f in files:
|
|
380
|
+
f.block = pos if f.size else 0
|
|
381
|
+
pos += _sectors(f.size)
|
|
382
|
+
mirror = _align(pos, META_UNIT)
|
|
383
|
+
length = mirror + meta_blocks
|
|
384
|
+
return {"dirs": dirs, "files": files, "meta_blocks": meta_blocks, "mirror": mirror,
|
|
385
|
+
"data_start": META_START + meta_blocks, "data_end": pos,
|
|
386
|
+
"partition_length": length, "total": PARTITION_START + length + 1,
|
|
387
|
+
"next_unique_id": uid}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _metadata(root, layout, label, date):
|
|
391
|
+
"""The metadata file's blocks."""
|
|
392
|
+
meta = bytearray(layout["meta_blocks"] * SECTOR)
|
|
393
|
+
|
|
394
|
+
def put(block, data):
|
|
395
|
+
meta[block * SECTOR:block * SECTOR + len(data)] = data
|
|
396
|
+
|
|
397
|
+
put(0, file_set(label, root.fe_block, date))
|
|
398
|
+
put(1, terminating(1))
|
|
399
|
+
for d in layout["dirs"]:
|
|
400
|
+
links = 1 + sum(1 for c in d.children if c.is_dir)
|
|
401
|
+
first = d.fe_block + 1
|
|
402
|
+
put(d.fe_block, file_entry(d.fe_block, True, d.dir_bytes, first, d.unique_id, links,
|
|
403
|
+
date))
|
|
404
|
+
parent = d.parent or d
|
|
405
|
+
fids = bytearray(file_identifier(first, None, FID_DIRECTORY | FID_PARENT,
|
|
406
|
+
parent.fe_block, parent.unique_id))
|
|
407
|
+
for c in d.children:
|
|
408
|
+
fids += file_identifier(first + len(fids) // SECTOR, c.name,
|
|
409
|
+
FID_DIRECTORY if c.is_dir else 0, c.fe_block, c.unique_id)
|
|
410
|
+
put(first, fids)
|
|
411
|
+
for f in layout["files"]:
|
|
412
|
+
put(f.fe_block, file_entry(f.fe_block, False, f.size, f.block, f.unique_id, 1, date))
|
|
413
|
+
return bytes(meta)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _write(root, layout, out_path, label, date, on_progress):
|
|
417
|
+
p_start, p_len = PARTITION_START, layout["partition_length"]
|
|
418
|
+
head = bytearray((p_start + META_START) * SECTOR)
|
|
419
|
+
|
|
420
|
+
def put(sector, data):
|
|
421
|
+
head[sector * SECTOR:sector * SECTOR + len(data)] = data
|
|
422
|
+
|
|
423
|
+
for i, ident in enumerate((b"BEA01", b"NSR03", b"TEA01")):
|
|
424
|
+
put(VRS_SECTOR + i, volume_recognition(ident))
|
|
425
|
+
utc = date if date.tzinfo else date.replace(tzinfo=datetime.timezone.utc)
|
|
426
|
+
volume_set = "%08X%08X" % (int(utc.timestamp()) & 0xFFFFFFFF,
|
|
427
|
+
zlib.crc32(label.encode("ascii")))
|
|
428
|
+
for base in (MAIN_VDS_SECTOR, RESERVE_VDS_SECTOR):
|
|
429
|
+
put(base + 0, primary_volume(base + 0, label, volume_set, date))
|
|
430
|
+
put(base + 1, implementation_use_volume(base + 1, label))
|
|
431
|
+
put(base + 2, partition(base + 2, p_start, p_len))
|
|
432
|
+
put(base + 3, logical_volume(base + 3, label, 0, 1))
|
|
433
|
+
put(base + 4, unallocated_space(base + 4))
|
|
434
|
+
put(base + 5, terminating(base + 5))
|
|
435
|
+
put(LVIS_SECTOR, logical_volume_integrity(
|
|
436
|
+
LVIS_SECTOR, date, layout["next_unique_id"], [p_len, layout["meta_blocks"]],
|
|
437
|
+
len(layout["files"]), len(layout["dirs"])))
|
|
438
|
+
put(LVIS_SECTOR + 1, terminating(LVIS_SECTOR + 1))
|
|
439
|
+
put(ANCHOR_SECTOR, anchor_pointer(ANCHOR_SECTOR))
|
|
440
|
+
put(p_start + 0, metadata_file_entry(0, FILE_TYPE_METADATA, META_START,
|
|
441
|
+
layout["meta_blocks"], date))
|
|
442
|
+
put(p_start + 1, metadata_file_entry(1, FILE_TYPE_METADATA_MIRROR, layout["mirror"],
|
|
443
|
+
layout["meta_blocks"], date))
|
|
444
|
+
meta = _metadata(root, layout, label, date)
|
|
445
|
+
|
|
446
|
+
total_bytes = layout["total"] * SECTOR
|
|
447
|
+
done = 0
|
|
448
|
+
|
|
449
|
+
def progress(n):
|
|
450
|
+
nonlocal done
|
|
451
|
+
done += n
|
|
452
|
+
if on_progress:
|
|
453
|
+
on_progress(min(1.0, done / total_bytes))
|
|
454
|
+
|
|
455
|
+
zeros = bytes(_CHUNK)
|
|
456
|
+
with open(out_path, "wb") as out:
|
|
457
|
+
out.write(head)
|
|
458
|
+
out.write(meta)
|
|
459
|
+
progress(len(head) + len(meta))
|
|
460
|
+
for f in layout["files"]:
|
|
461
|
+
if not f.size:
|
|
462
|
+
continue
|
|
463
|
+
_copy_file(f, out, progress)
|
|
464
|
+
pad = -f.size % SECTOR
|
|
465
|
+
out.write(zeros[:pad])
|
|
466
|
+
progress(pad)
|
|
467
|
+
gap = (layout["mirror"] - layout["data_end"]) * SECTOR
|
|
468
|
+
out.write(zeros[:gap] if gap <= len(zeros) else bytes(gap))
|
|
469
|
+
out.write(meta)
|
|
470
|
+
out.write(anchor_pointer(layout["total"] - 1).ljust(SECTOR, b"\0"))
|
|
471
|
+
progress(gap + len(meta) + SECTOR)
|
|
472
|
+
if out.tell() != total_bytes:
|
|
473
|
+
raise UdfError(f"internal error: wrote {out.tell()} bytes, "
|
|
474
|
+
f"expected {total_bytes}")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _copy_file(node, out, progress):
|
|
478
|
+
remaining = node.size
|
|
479
|
+
try:
|
|
480
|
+
with open(node.src, "rb") as f:
|
|
481
|
+
while remaining > 0:
|
|
482
|
+
chunk = f.read(min(_CHUNK, remaining))
|
|
483
|
+
if not chunk:
|
|
484
|
+
break
|
|
485
|
+
out.write(chunk)
|
|
486
|
+
remaining -= len(chunk)
|
|
487
|
+
progress(len(chunk))
|
|
488
|
+
extra = f.read(1)
|
|
489
|
+
except OSError as e:
|
|
490
|
+
raise UdfError(f"cannot read {node.src}: {e.strerror}") from e
|
|
491
|
+
if remaining or extra:
|
|
492
|
+
raise UdfError(f"{node.src} changed size while the image was written")
|