Skip to content

Commit b4f7484

Browse files
committed
Add RR support to add_file InPlaceEditor.
Signed-off-by: Chris Lalancette <clalancette@gmail.com>
1 parent fbaf48e commit b4f7484

2 files changed

Lines changed: 116 additions & 25 deletions

File tree

pycdlib/inplaceeditor.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ def _do_rm_file(iso, iso_path):
288288

289289

290290
def _do_add_fp(iso, data_source, length, manage_fp, iso_path,
291-
joliet_path=None, file_mode=None):
292-
# type: (PyCdlib, Union[BinaryIO, str], int, bool, str, Optional[str], Optional[int]) -> None
291+
rr_name=None, joliet_path=None, file_mode=None):
292+
# type: (PyCdlib, Union[BinaryIO, str], int, bool, str, Optional[str], Optional[str], Optional[int]) -> None
293293
"""
294294
Implementation of in-place file addition. Operates on an open
295295
:class:`pycdlib.PyCdlib` object's internal state; not a public API.
@@ -302,10 +302,14 @@ def _do_add_fp(iso, data_source, length, manage_fp, iso_path,
302302
extent must have enough slack to hold one more record.
303303
304304
Refuses on:
305-
- UDF, Rock Ridge, or El Torito ISOs (out of scope for v1).
305+
- UDF or El Torito ISOs (out of scope for v1).
306306
- A target path whose parent's directory extent would need to
307307
grow to fit the new record (would require relocating the parent,
308308
cascading layout changes).
309+
- Rock Ridge metadata that would require allocating a new
310+
Continuation Entry (CE) block -- this happens when the SUSP
311+
fields don't fit inside the directory record (e.g., very long
312+
``rr_name``). Short-name Rock Ridge is supported.
309313
"""
310314
if not iso._initialized: # pylint: disable=protected-access
311315
raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
@@ -315,11 +319,16 @@ def _do_add_fp(iso, data_source, length, manage_fp, iso_path,
315319

316320
if iso.udf_root is not None:
317321
raise pycdlibexception.PyCdlibInvalidInput('InPlaceEditor.add_fp does not support UDF ISOs yet; use PyCdlib.add_fp + write_fp() to produce a new ISO instead')
318-
if iso.rock_ridge:
319-
raise pycdlibexception.PyCdlibInvalidInput('InPlaceEditor.add_fp does not support Rock Ridge ISOs yet; use PyCdlib.add_fp + write_fp() to produce a new ISO instead')
320322
if iso.eltorito_boot_catalog is not None:
321323
raise pycdlibexception.PyCdlibInvalidInput('InPlaceEditor.add_fp does not support El Torito ISOs yet; use PyCdlib.add_fp + write_fp() to produce a new ISO instead')
322324

325+
# Rock Ridge: rr_name must be present iff the ISO is Rock Ridge,
326+
# matching PyCdlib.add_fp's contract.
327+
if iso.rock_ridge and rr_name is None:
328+
raise pycdlibexception.PyCdlibInvalidInput('Must specify an rr_name when adding a file to a Rock Ridge ISO')
329+
if not iso.rock_ridge and rr_name is not None:
330+
raise pycdlibexception.PyCdlibInvalidInput('Cannot specify an rr_name on a non-Rock-Ridge ISO')
331+
323332
# Resolve the ISO9660 path's parent and leaf name.
324333
iso_path_bytes = utils.normpath(iso_path)
325334
(iso_name, iso_parent) = iso._iso_name_and_parent_from_path(iso_path_bytes) # pylint: disable=protected-access
@@ -336,26 +345,40 @@ def _do_add_fp(iso, data_source, length, manage_fp, iso_path,
336345
# Reserve the new file's data extent at the current end of the ISO.
337346
new_data_extent = iso.pvd.space_size
338347

339-
fmode = file_mode if file_mode is not None else 0
348+
# Default Rock Ridge file mode matches PyCdlib's add_fp default
349+
# for managed-fp callers (regular file, r--r--r--).
350+
if file_mode is None:
351+
fmode = 0o0100444 if iso.rock_ridge else 0
352+
else:
353+
fmode = file_mode
340354

341355
# Create the shared Inode that backs every linked record.
342356
ino = inode.Inode()
343357
ino.new(length, data_source, manage_fp, 0)
344358
ino.set_extent_location(new_data_extent)
345359

346-
# Create the ISO9660 directory record, link it to the inode, and
347-
# attempt to add it to the parent's children list. add_child
348-
# returns True if the parent would have to grow its data_length to
349-
# fit the new record -- in-place is a fixed-allocation primitive,
350-
# so we treat that as a hard failure and roll the change back.
360+
# Create the ISO9660 directory record. For a Rock Ridge ISO we
361+
# pass the version and rr_name through so the SUSP entries land in
362+
# the record; for a plain ISO9660 ISO we pass empty placeholders.
363+
rr_version = iso.rock_ridge if iso.rock_ridge else ''
364+
rr_name_bytes = rr_name.encode('utf-8') if rr_name is not None else b''
365+
351366
new_iso_rec = dr.DirectoryRecord()
352367
new_iso_rec.new_file(iso.pvd, length, iso_name, iso_parent,
353368
iso.pvd.sequence_number(),
354-
'', b'', iso.xa, fmode,
369+
rr_version, rr_name_bytes, iso.xa, fmode,
355370
time.time(), None)
356371
new_iso_rec.set_data_location(new_data_extent, 0)
357372
new_iso_rec.inode = ino
358373

374+
# If Rock Ridge couldn't fit all its SUSP fields inline, the
375+
# record now has a CE (Continuation Entry) referencing a separate
376+
# block. In-place add can't grow the on-disk RR CE allocation, so
377+
# we refuse the add up front. Catch this *before* mutating the
378+
# parent so there's nothing to roll back.
379+
if new_iso_rec.rock_ridge is not None and new_iso_rec.rock_ridge.dr_entries.ce_record is not None:
380+
raise pycdlibexception.PyCdlibInvalidInput("Adding this file requires a Rock Ridge Continuation Entry (CE) block, which in-place add cannot allocate; use PyCdlib.add_fp + write_fp() to produce a new ISO instead")
381+
359382
iso_overflowed = iso_parent.add_child(new_iso_rec, iso.logical_block_size)
360383
if iso_overflowed:
361384
iso_parent.remove_child(new_iso_rec, new_iso_rec.index_in_parent, iso.logical_block_size)
@@ -517,8 +540,9 @@ def rm_file(self, iso_path):
517540
"""
518541
_do_rm_file(self._iso, iso_path)
519542

520-
def add_fp(self, fp, length, iso_path, joliet_path=None, file_mode=None):
521-
# type: (BinaryIO, int, str, Optional[str], Optional[int]) -> None
543+
def add_fp(self, fp, length, iso_path, rr_name=None, joliet_path=None,
544+
file_mode=None):
545+
# type: (BinaryIO, int, str, Optional[str], Optional[str], Optional[int]) -> None
522546
"""
523547
Add a new file to the ISO in place, reading the content from
524548
a file-like object.
@@ -529,8 +553,12 @@ def add_fp(self, fp, length, iso_path, joliet_path=None, file_mode=None):
529553
existing extent on disk.
530554
531555
Constraints:
532-
- The ISO must not be a UDF, Rock Ridge, or El Torito ISO
533-
(out of scope for v1).
556+
- The ISO must not be a UDF or El Torito ISO (out of scope
557+
for v1).
558+
- On a Rock Ridge ISO, ``rr_name`` must be provided. Rock
559+
Ridge SUSP fields whose serialized length would require a
560+
new Continuation Entry block are refused (this typically
561+
means very long ``rr_name`` values; short names are fine).
534562
- The target's parent directory must already exist on the ISO.
535563
- The parent's directory extent must have enough slack to
536564
hold one more record; if not, this raises and you must use
@@ -543,18 +571,22 @@ def add_fp(self, fp, length, iso_path, joliet_path=None, file_mode=None):
543571
length - The length of the new content.
544572
iso_path - The ISO9660 absolute path identifying where to
545573
insert the file.
574+
rr_name - Rock Ridge name; required on Rock Ridge ISOs and
575+
rejected on non-Rock-Ridge ISOs.
546576
joliet_path - The Joliet absolute path; if provided, a Joliet
547577
directory record is also inserted.
548-
file_mode - File-mode bits (ignored on non-Rock-Ridge ISOs;
549-
since v1 refuses Rock Ridge, this is informational).
578+
file_mode - File-mode bits (Rock Ridge only).
550579
Returns:
551580
Nothing.
552581
"""
553582
_do_add_fp(self._iso, fp, length, False, iso_path,
554-
joliet_path=joliet_path, file_mode=file_mode)
583+
rr_name=rr_name,
584+
joliet_path=joliet_path,
585+
file_mode=file_mode)
555586

556-
def add_file(self, filename, iso_path, joliet_path=None, file_mode=None):
557-
# type: (str, str, Optional[str], Optional[int]) -> None
587+
def add_file(self, filename, iso_path, rr_name=None, joliet_path=None,
588+
file_mode=None):
589+
# type: (str, str, Optional[str], Optional[str], Optional[int]) -> None
558590
"""
559591
Add a new file to the ISO in place, reading the content from
560592
a file on the local filesystem.
@@ -569,12 +601,17 @@ def add_file(self, filename, iso_path, joliet_path=None, file_mode=None):
569601
file's content on the ISO.
570602
iso_path - The ISO9660 absolute path identifying where to
571603
insert the file.
604+
rr_name - Rock Ridge name; required on Rock Ridge ISOs and
605+
rejected on non-Rock-Ridge ISOs.
572606
joliet_path - The Joliet absolute path; if provided, a Joliet
573607
directory record is also inserted.
574-
file_mode - File-mode bits (ignored on non-Rock-Ridge ISOs).
608+
file_mode - File-mode bits (Rock Ridge only).
575609
Returns:
576610
Nothing.
577611
"""
578612
import os # pylint: disable=import-outside-toplevel
579613
_do_add_fp(self._iso, filename, os.stat(filename).st_size, True,
580-
iso_path, joliet_path=joliet_path, file_mode=file_mode)
614+
iso_path,
615+
rr_name=rr_name,
616+
joliet_path=joliet_path,
617+
file_mode=file_mode)

tests/integration/test_new.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,7 +2878,35 @@ def test_new_in_place_editor_add_fp_rejects_udf(tmpdir):
28782878
ed.add_fp(io.BytesIO(b'new\n'), 4, '/NEW.;1')
28792879
assert 'UDF' in str(excinfo.value)
28802880

2881-
def test_new_in_place_editor_add_fp_rejects_rock_ridge(tmpdir):
2881+
def test_new_in_place_editor_add_fp_rock_ridge_short_name(tmpdir):
2882+
# Rock Ridge ISOs are supported as long as the SUSP fields fit
2883+
# inside the directory record (i.e., no Continuation Entry block
2884+
# needs to be allocated). Short rr_name values comfortably fit.
2885+
iso_path = str(tmpdir.join('test.iso'))
2886+
iso = pycdlib.PyCdlib()
2887+
iso.new(rock_ridge='1.09')
2888+
iso.add_fp(io.BytesIO(b'old\n'), 4, '/OLD.;1', rr_name='old')
2889+
with open(iso_path, 'wb') as f:
2890+
iso.write_fp(f)
2891+
iso.close()
2892+
2893+
with pycdlib.InPlaceEditor(iso_path) as ed:
2894+
ed.add_fp(io.BytesIO(b'new contents\n'), 13, '/NEW.;1', rr_name='new')
2895+
2896+
iso2 = pycdlib.PyCdlib()
2897+
iso2.open(iso_path)
2898+
buf = io.BytesIO()
2899+
iso2.get_file_from_iso_fp(buf, iso_path='/NEW.;1')
2900+
assert buf.getvalue() == b'new contents\n'
2901+
buf = io.BytesIO()
2902+
iso2.get_file_from_iso_fp(buf, rr_path='/new')
2903+
assert buf.getvalue() == b'new contents\n'
2904+
iso2.close()
2905+
2906+
def test_new_in_place_editor_add_fp_rejects_rock_ridge_ce_required(tmpdir):
2907+
# When the rr_name is long enough to push the SUSP fields out of
2908+
# the directory record into a Continuation Entry block, in-place
2909+
# add can't allocate the CE storage and must refuse.
28822910
iso_path = str(tmpdir.join('test.iso'))
28832911
iso = pycdlib.PyCdlib()
28842912
iso.new(rock_ridge='1.09')
@@ -2889,8 +2917,34 @@ def test_new_in_place_editor_add_fp_rejects_rock_ridge(tmpdir):
28892917

28902918
with pycdlib.InPlaceEditor(iso_path) as ed:
28912919
with pytest.raises(pycdlib.pycdlibexception.PyCdlibInvalidInput) as excinfo:
2920+
ed.add_fp(io.BytesIO(b'x'), 1, '/NEW.;1', rr_name='a' * 200)
2921+
assert 'Continuation Entry' in str(excinfo.value)
2922+
2923+
def test_new_in_place_editor_add_fp_rock_ridge_requires_rr_name(tmpdir):
2924+
iso_path = str(tmpdir.join('test.iso'))
2925+
iso = pycdlib.PyCdlib()
2926+
iso.new(rock_ridge='1.09')
2927+
iso.add_fp(io.BytesIO(b'old\n'), 4, '/OLD.;1', rr_name='old')
2928+
with open(iso_path, 'wb') as f:
2929+
iso.write_fp(f)
2930+
iso.close()
2931+
2932+
with pycdlib.InPlaceEditor(iso_path) as ed:
2933+
with pytest.raises(pycdlib.pycdlibexception.PyCdlibInvalidInput):
28922934
ed.add_fp(io.BytesIO(b'new\n'), 4, '/NEW.;1')
2893-
assert 'Rock Ridge' in str(excinfo.value)
2935+
2936+
def test_new_in_place_editor_add_fp_rejects_rr_name_on_non_rock_ridge(tmpdir):
2937+
iso_path = str(tmpdir.join('test.iso'))
2938+
iso = pycdlib.PyCdlib()
2939+
iso.new()
2940+
iso.add_fp(io.BytesIO(b'old\n'), 4, '/OLD.;1')
2941+
with open(iso_path, 'wb') as f:
2942+
iso.write_fp(f)
2943+
iso.close()
2944+
2945+
with pycdlib.InPlaceEditor(iso_path) as ed:
2946+
with pytest.raises(pycdlib.pycdlibexception.PyCdlibInvalidInput):
2947+
ed.add_fp(io.BytesIO(b'new\n'), 4, '/NEW.;1', rr_name='new')
28942948

28952949
def test_new_in_place_editor_add_fp_rejects_eltorito(tmpdir):
28962950
iso_path = str(tmpdir.join('test.iso'))

0 commit comments

Comments
 (0)