From b4ec081b9e49c06d70e8aed9ef1820855372be82 Mon Sep 17 00:00:00 2001 From: Samuel Carlsson Date: Wed, 8 Oct 2025 08:40:07 +0200 Subject: [PATCH 1/5] `seek` now correctly returns position after seeking --- ar/substream.py | 3 ++- ar/tests/test_bsd.py | 2 +- ar/tests/test_linux.py | 2 +- ar/tests/test_windows.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ar/substream.py b/ar/substream.py index b3e371b..ad5e23b 100644 --- a/ar/substream.py +++ b/ar/substream.py @@ -8,7 +8,7 @@ def __init__(self, file: io.RawIOBase, start: int, size: int): self.size = size self.position = 0 - def seek(self, offset, origin=0): + def seek(self, offset, origin=0) -> int: if origin == 0: self.position = offset elif origin == 1: @@ -17,6 +17,7 @@ def seek(self, offset, origin=0): self.position = self.size + offset else: raise ValueError(f"Unexpected origin: {origin}") + return self.position def seekable(self): return True diff --git a/ar/tests/test_bsd.py b/ar/tests/test_bsd.py index 78d36ab..b931135 100644 --- a/ar/tests/test_bsd.py +++ b/ar/tests/test_bsd.py @@ -42,5 +42,5 @@ def test_seek_basic(): with ARCHIVE.open('rb') as f: archive = Archive(f) file0 = archive.open('file0.txt') - file0.seek(1) + assert file0.seek(1) == 1 assert file0.read(3) == 'ell' diff --git a/ar/tests/test_linux.py b/ar/tests/test_linux.py index 6feca1a..0f4ec4c 100644 --- a/ar/tests/test_linux.py +++ b/ar/tests/test_linux.py @@ -35,7 +35,7 @@ def test_seek_basic(): with ARCHIVE.open('rb') as f: archive = Archive(f) file0 = archive.open('file0.txt') - file0.seek(1) + assert file0.seek(1) == 1 assert file0.read(3) == 'ell' diff --git a/ar/tests/test_windows.py b/ar/tests/test_windows.py index 04b4b40..6e80cdc 100644 --- a/ar/tests/test_windows.py +++ b/ar/tests/test_windows.py @@ -29,7 +29,7 @@ def test_seek_basic(): with ARCHIVE.open('rb') as f: archive = Archive(f) file0 = archive.open('x64\\Release\\MiniLib.obj', 'rb') - file0.seek(1) + assert file0.seek(1) == 1 assert file0.read(3) == b'\x00\xff\xff' From 98279f7c64ffdbce519d65ac88bb66f8b68a2ec1 Mon Sep 17 00:00:00 2001 From: Samuel Carlsson Date: Wed, 8 Oct 2025 09:48:56 +0200 Subject: [PATCH 2/5] Specific test module for seek tests --- ar/tests/test_bsd.py | 2 +- ar/tests/test_linux.py | 2 +- ar/tests/test_seek.py | 36 ++++++++++++++++++++++++++++++++++++ ar/tests/test_windows.py | 2 +- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 ar/tests/test_seek.py diff --git a/ar/tests/test_bsd.py b/ar/tests/test_bsd.py index b931135..78d36ab 100644 --- a/ar/tests/test_bsd.py +++ b/ar/tests/test_bsd.py @@ -42,5 +42,5 @@ def test_seek_basic(): with ARCHIVE.open('rb') as f: archive = Archive(f) file0 = archive.open('file0.txt') - assert file0.seek(1) == 1 + file0.seek(1) assert file0.read(3) == 'ell' diff --git a/ar/tests/test_linux.py b/ar/tests/test_linux.py index 0f4ec4c..6feca1a 100644 --- a/ar/tests/test_linux.py +++ b/ar/tests/test_linux.py @@ -35,7 +35,7 @@ def test_seek_basic(): with ARCHIVE.open('rb') as f: archive = Archive(f) file0 = archive.open('file0.txt') - assert file0.seek(1) == 1 + file0.seek(1) assert file0.read(3) == 'ell' diff --git a/ar/tests/test_seek.py b/ar/tests/test_seek.py new file mode 100644 index 0000000..3f8cbdb --- /dev/null +++ b/ar/tests/test_seek.py @@ -0,0 +1,36 @@ +# pylint: disable=redefined-outer-name +from pathlib import Path + +from ar import Archive + + +ARCHIVE = Path('test_data/linux.a') + + +def test_seek_from_start(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + file0.seek(2) + assert file0.tell() == 2 + assert file0.read(1) == 'l' + + +def test_seek_from_current(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + assert file0.read(1) == 'H' + file0.seek(1, 1) + assert file0.tell() == 2 + assert file0.read(2) == 'll' + + +def test_seek_from_end(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + file_size = 5 + file0.seek(-2, 2) + assert file0.tell() == file_size - 2 + assert file0.read() == 'lo' diff --git a/ar/tests/test_windows.py b/ar/tests/test_windows.py index 6e80cdc..04b4b40 100644 --- a/ar/tests/test_windows.py +++ b/ar/tests/test_windows.py @@ -29,7 +29,7 @@ def test_seek_basic(): with ARCHIVE.open('rb') as f: archive = Archive(f) file0 = archive.open('x64\\Release\\MiniLib.obj', 'rb') - assert file0.seek(1) == 1 + file0.seek(1) assert file0.read(3) == b'\x00\xff\xff' From 21c45da46beb9b58be1a33d9c90c19dda3ac60b6 Mon Sep 17 00:00:00 2001 From: Samuel Carlsson Date: Wed, 8 Oct 2025 10:47:13 +0200 Subject: [PATCH 3/5] avoid creating untracked files during testing --- .gitignore | 2 -- ar/tests/test_roundtrip.py | 27 ++++++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index c85bf12..3c665fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ __pycache__/ /*.egg-info -/test_data/file0.txt -/test_data/file1.bin /test_data/test.a /dist/ diff --git a/ar/tests/test_roundtrip.py b/ar/tests/test_roundtrip.py index 91bf802..7620014 100644 --- a/ar/tests/test_roundtrip.py +++ b/ar/tests/test_roundtrip.py @@ -1,5 +1,6 @@ # pylint: disable=redefined-outer-name import subprocess +import tempfile from pathlib import Path import pytest @@ -12,15 +13,23 @@ @pytest.fixture def simple_archive(): - # Create archive - TEST_DATA.mkdir(exist_ok=True) - - (TEST_DATA / 'file0.txt').write_text('Hello') - (TEST_DATA / 'file1.bin').write_bytes(b'\xc3\x28') # invalid utf-8 characters - (TEST_DATA / 'long_file_name_test0.txt').write_text('Hello2') - (TEST_DATA / 'long_file_name_test1.bin').write_bytes(b'\xc3\x28') - subprocess.check_call('ar r test.a file0.txt file1.bin long_file_name_test0.txt long_file_name_test1.bin'.split(), cwd=str(TEST_DATA)) - return TEST_DATA / 'test.a' + archive_path = TEST_DATA / 'test.a' + if archive_path.exists(): + return archive_path + + archive_full_path = archive_path.resolve() + with tempfile.TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + (tmp_path / 'file0.txt').write_text('Hello') + (tmp_path / 'file1.bin').write_bytes(b'\xc3\x28') # invalid utf-8 characters + (tmp_path / 'long_file_name_test0.txt').write_text('Hello2') + (tmp_path / 'long_file_name_test1.bin').write_bytes(b'\xc3\x28') + subprocess.check_call( + ['ar', 'r', str(archive_full_path), 'file0.txt', 'file1.bin', 'long_file_name_test0.txt', 'long_file_name_test1.bin'], + cwd=tmpdir, + ) + + return archive_path def test_list(simple_archive): From d93f414e1cd5c241c5c6a8355e4f67a2f8413461 Mon Sep 17 00:00:00 2001 From: Samuel Carlsson Date: Wed, 8 Oct 2025 10:52:24 +0200 Subject: [PATCH 4/5] add test for `.seekable()` --- ar/tests/test_seek.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ar/tests/test_seek.py b/ar/tests/test_seek.py index 3f8cbdb..7f3defb 100644 --- a/ar/tests/test_seek.py +++ b/ar/tests/test_seek.py @@ -7,6 +7,13 @@ ARCHIVE = Path('test_data/linux.a') +def test_seekable(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + assert file0.seekable() + + def test_seek_from_start(): with ARCHIVE.open('rb') as f: archive = Archive(f) From a17ba74399ad25ef2d1df3f25a7e2b707b813273 Mon Sep 17 00:00:00 2001 From: Samuel Carlsson Date: Wed, 8 Oct 2025 11:06:23 +0200 Subject: [PATCH 5/5] check bounds when seeking --- ar/substream.py | 12 +++++++++--- ar/tests/test_seek.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/ar/substream.py b/ar/substream.py index ad5e23b..9e02c37 100644 --- a/ar/substream.py +++ b/ar/substream.py @@ -1,3 +1,4 @@ +import errno import io class Substream(io.RawIOBase): @@ -10,13 +11,18 @@ def __init__(self, file: io.RawIOBase, start: int, size: int): def seek(self, offset, origin=0) -> int: if origin == 0: - self.position = offset + position = offset elif origin == 1: - self.position += offset + position = self.position + offset elif origin == 2: - self.position = self.size + offset + position = self.size + offset else: raise ValueError(f"Unexpected origin: {origin}") + + if position < 0 or position > self.size: + raise OSError(errno.EINVAL, "Invalid argument") + + self.position = position return self.position def seekable(self): diff --git a/ar/tests/test_seek.py b/ar/tests/test_seek.py index 7f3defb..08cb052 100644 --- a/ar/tests/test_seek.py +++ b/ar/tests/test_seek.py @@ -1,6 +1,9 @@ # pylint: disable=redefined-outer-name +import errno from pathlib import Path +import pytest + from ar import Archive @@ -41,3 +44,34 @@ def test_seek_from_end(): file0.seek(-2, 2) assert file0.tell() == file_size - 2 assert file0.read() == 'lo' + + +def test_seek_before_start_raises_oserror(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + with pytest.raises(OSError) as excinfo: + file0.seek(-1) + assert excinfo.value.errno == errno.EINVAL + assert file0.tell() == 0 + + +def test_seek_beyond_end_raises_oserror(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + with pytest.raises(OSError) as excinfo: + file0.seek(6, 0) + assert excinfo.value.errno == errno.EINVAL + assert file0.tell() == 0 + + +def test_seek_from_current_outside_bounds_raises_oserror(): + with ARCHIVE.open('rb') as f: + archive = Archive(f) + file0 = archive.open('file0.txt', 'r') + assert file0.read(1) == 'H' + with pytest.raises(OSError) as excinfo: + file0.seek(-2, 1) + assert excinfo.value.errno == errno.EINVAL + assert file0.tell() == 1