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 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")