Repository: vidstige/ar

Issue:
Possible fix for GNU extended filenames
Looks like this line:
https://github.com/vidstige/ar/blob/e0275f79281c1cc5d9f3f4622b8de3f76cf95a42/ar/archive.py#L78

Should be:
```py
end = data.find(b"\n", start)
```

After this fix, I was able to work with my files.
@vidstige please check it out

Hints from issue comments:
@vidstige: Thanks for your contribution! But I think this will break tar files that does not have the `\n` character? Have you run the tests? I think maybe a better solution is to `.rstrip('\n')` characters from the filename instead to be compatible with even more tar-files.@vidstige: I just tested and this break Windows files and more.@vidstige: If you change something, please create a minimal tar-file that you can commit to the `test_data/` directory. I can do it for you if you tell me how you created this file. There is already a large coverage in there
* Windows lib-files
* Tar files produced on Mac
* Tar files produced on Linux
* Tar files produced on bsd
@vshymanskyy: Thanks for checking it out.

Here's a simple example that reproduces the issue:
[reproduce.zip](https://github.com/user-attachments/files/16962833/reproduce.zip)

When you run `list_entries.py`, you'll notice that long filenames are parsed incorrectly, the first erroneous item would look like:
```log
Entry: lib_a-cephes_subr.o/
lib_a-cephes_subrf.o/
lib_a-cephes_subrl.o/
lib_a-copysignl.o/
lib_a-e_rem_pio2.o/
lib_a-e_remainder.o/
lib_a-ef_acosh.o/
lib_a-ef_atan2.o/
lib_a-ef_atanh.o/
lib_a-ef_hypot.o/
lib_a-ef_log10.o/
lib_a-ef_rem_pio2.o/
lib_a-ef_remainder.o/
lib_a-ef_scalb.o/
lib_a-el_hypot.o/
....
```

Note, this is  a single entry with a multi-line filename.
The expected name for this is entry is just `lib_a-cephes_subr.o`, this is why I submitted the fix proposal.
But obviously I missed something important. Maybe, we should look for `/` (0x2F) instead of `0x00`. The wiki says:
> System V ar uses a '/' character (0x2F) to mark the end of the filename

The `libm.a` that I've included with the test is the one shipped with `gcc-arm-none-eabi` package on Ubuntu and can be found in `/usr/lib/arm-none-eabi/newlib/thumb/v6-m/nofp/libm.a`

Other than that, the `ar` package works correctly and I'm able to use it to link real binaries@vshymanskyy: @vidstige  also, looks like this is not the expected result:
https://github.com/vidstige/ar/blob/e0275f79281c1cc5d9f3f4622b8de3f76cf95a42/ar/tests/test_windows.py#L16

I'd expect file extension to be `.obj`@vshymanskyy: @vidstige I have reopened the PR with the new suggested fix: https://github.com/vidstige/ar/pull/17/files
It seems to pass the tests (and also fixes Windows tests)

Solution patch:
diff --git a/ar/archive.py b/ar/archive.py
index 176c651..ac36ab5 100644
--- a/ar/archive.py
+++ b/ar/archive.py
@@ -74,9 +74,11 @@ def open(self, path: Union[str, ArPath], mode='r', encoding='utf-8'):
 
 
 def lookup(data: bytes, offset: int) -> str:
-    start = offset
-    end = data.find(b"\00", start)
-    return data[start:end - 1].decode()
+    for delim in (b"\x00", b"\x2F"):
+        pos = data.find(delim, offset)
+        if pos >= 0:
+            data = data[offset:pos]
+    return data.decode()
 
 
 ENTRY_FORMAT = '16s12s6s6s8s10sbb'


Test patch:
diff --git a/ar/tests/test_roundtrip.py b/ar/tests/test_roundtrip.py
index 5b3dc0c..91bf802 100644
--- a/ar/tests/test_roundtrip.py
+++ b/ar/tests/test_roundtrip.py
@@ -17,14 +17,16 @@ def simple_archive():
 
     (TEST_DATA / 'file0.txt').write_text('Hello')
     (TEST_DATA / 'file1.bin').write_bytes(b'\xc3\x28')  # invalid utf-8 characters
-    subprocess.check_call('ar r test.a file0.txt file1.bin'.split(), cwd=str(TEST_DATA))
+    (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'
 
 
 def test_list(simple_archive):
     with simple_archive.open('rb') as f:
         archive = Archive(f)
-        assert ['file0.txt', 'file1.bin'] == [entry.name for entry in archive]
+        assert ['file0.txt', 'file1.bin', 'long_file_name_test0.txt', 'long_file_name_test1.bin'] == [entry.name for entry in archive]
 
 
 def test_read_content(simple_archive):
@@ -41,6 +43,20 @@ def test_read_binary(simple_archive):
         file0 = archive.open('file1.bin', 'rb')
         assert file0.read() == b'\xc3\x28'
 
+def test_read_content_ext(simple_archive):
+    with simple_archive.open('rb') as f:
+        archive = Archive(f)
+        file0 = archive.open('long_file_name_test0.txt')
+        assert file0.read(2) == 'He'
+        assert file0.read() == 'llo2'
+
+
+def test_read_binary_ext(simple_archive):
+    with simple_archive.open('rb') as f:
+        archive = Archive(f)
+        file0 = archive.open('long_file_name_test1.bin', 'rb')
+        assert file0.read() == b'\xc3\x28'
+
 
 def test_seek_basic(simple_archive):
     with simple_archive.open('rb') as f:
diff --git a/ar/tests/test_windows.py b/ar/tests/test_windows.py
index c2e1be1..04b4b40 100644
--- a/ar/tests/test_windows.py
+++ b/ar/tests/test_windows.py
@@ -13,7 +13,7 @@
 def test_list():
     with ARCHIVE.open('rb') as f:
         archive = Archive(f)
-        expected = ['x64\\Release\\pch.ob', 'x64\\Release\\MiniLib.ob']
+        expected = ['x64\\Release\\pch.obj', 'x64\\Release\\MiniLib.obj']
         actual = [entry.name for entry in archive]
         assert actual == expected
 
@@ -21,14 +21,14 @@ def test_list():
 def test_read_binary():
     with ARCHIVE.open('rb') as f:
         archive = Archive(f)
-        file0 = archive.open('x64\\Release\\MiniLib.ob', 'rb')
+        file0 = archive.open('x64\\Release\\MiniLib.obj', 'rb')
         assert file0.read(16) == b'\x00\x00\xff\xff\x01\x00d\x86tF\xaee8\xfe\xb3\x0c'
 
 
 def test_seek_basic():
     with ARCHIVE.open('rb') as f:
         archive = Archive(f)
-        file0 = archive.open('x64\\Release\\MiniLib.ob', 'rb')
+        file0 = archive.open('x64\\Release\\MiniLib.obj', 'rb')
         file0.seek(1)
         assert file0.read(3) == b'\x00\xff\xff'
 
@@ -36,7 +36,7 @@ def test_seek_basic():
 def test_tell():
     with ARCHIVE.open('rb') as f:
         archive = Archive(f)
-        file0 = archive.open('x64\\Release\\MiniLib.ob', 'rb')
+        file0 = archive.open('x64\\Release\\MiniLib.obj', 'rb')
         assert file0.tell() == 0
         file0.read(2)
         assert file0.tell() == 2