Skip to content

Commit b13fdd0

Browse files
authored
fix: Missing symbol from native lib (#123)
* fix: Missing symbol * fix: Refactor * fix: Docs * fix: Format * ci: Revert "fix: Format - tests do not like format change " This reverts commit f9c977c. * fix: Pipeline trigger * fix: One more signature change * fix: Bump version number
1 parent 21f5be7 commit b13fdd0

File tree

4 files changed

+95
-160
lines changed

4 files changed

+95
-160
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "c2pa-python"
7-
version = "0.11.0"
7+
version = "0.11.1"
88
requires-python = ">=3.10"
99
description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
1010
readme = { file = "README.md", content-type = "text/markdown" }

src/c2pa/c2pa.py

Lines changed: 50 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,37 +1725,35 @@ def to_archive(self, stream: Any):
17251725
raise C2paError(
17261726
self._error_messages['archive_error'].format("Unknown error"))
17271727

1728-
def sign(
1728+
def _sign_internal(
17291729
self,
17301730
signer: Signer,
17311731
format: str,
1732-
source: Any,
1733-
dest: Any = None) -> Optional[bytes]:
1734-
"""Sign the builder's content and write to a destination stream.
1732+
source_stream: Stream,
1733+
dest_stream: Stream) -> int:
1734+
"""Internal signing logic shared between sign() and sign_file() methods,
1735+
to use same native calls but expose different API surface.
17351736
17361737
Args:
1737-
format: The MIME type or extension of the content
1738-
source: The source stream (any Python stream-like object)
1739-
dest: The destination stream (any Python stream-like object)
17401738
signer: The signer to use
1739+
format: The MIME type or extension of the content
1740+
source_stream: The source stream
1741+
dest_stream: The destination stream
17411742
17421743
Returns:
1743-
A tuple of (size of C2PA data, optional manifest bytes)
1744+
Size of C2PA data
17441745
17451746
Raises:
17461747
C2paError: If there was an error during signing
17471748
"""
17481749
if not self._builder:
17491750
raise C2paError(self._error_messages['closed_error'])
17501751

1751-
# Convert Python streams to Stream objects
1752-
source_stream = Stream(source)
1753-
dest_stream = Stream(dest)
1754-
17551752
try:
17561753
format_str = format.encode('utf-8')
17571754
manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)()
17581755

1756+
# c2pa_builder_sign uses streams
17591757
result = _lib.c2pa_builder_sign(
17601758
self._builder,
17611759
format_str,
@@ -1770,66 +1768,72 @@ def sign(
17701768
if error:
17711769
raise C2paError(error)
17721770

1773-
manifest_bytes = None
17741771
if manifest_bytes_ptr:
1775-
# Convert the manifest bytes to a Python bytes object
1776-
size = result
1777-
manifest_bytes = bytes(manifest_bytes_ptr[:size])
1772+
# Free the manifest bytes pointer if it was allocated
17781773
_lib.c2pa_manifest_bytes_free(manifest_bytes_ptr)
17791774

1780-
return manifest_bytes
1775+
return result
17811776
finally:
17821777
# Ensure both streams are cleaned up
17831778
source_stream.close()
17841779
dest_stream.close()
17851780

1781+
def sign(
1782+
self,
1783+
signer: Signer,
1784+
format: str,
1785+
source: Any,
1786+
dest: Any = None) -> None:
1787+
"""Sign the builder's content and write to a destination stream.
1788+
1789+
Args:
1790+
format: The MIME type or extension of the content
1791+
source: The source stream (any Python stream-like object)
1792+
dest: The destination stream (any Python stream-like object)
1793+
signer: The signer to use
1794+
1795+
Raises:
1796+
C2paError: If there was an error during signing
1797+
"""
1798+
# Convert Python streams to Stream objects
1799+
source_stream = Stream(source)
1800+
dest_stream = Stream(dest)
1801+
1802+
# Use the internal stream-base signing logic
1803+
self._sign_internal(signer, format, source_stream, dest_stream)
1804+
17861805
def sign_file(self,
17871806
source_path: Union[str,
17881807
Path],
17891808
dest_path: Union[str,
17901809
Path],
1791-
signer: Signer) -> tuple[int,
1792-
Optional[bytes]]:
1810+
signer: Signer) -> int:
17931811
"""Sign a file and write the signed data to an output file.
17941812
17951813
Args:
17961814
source_path: Path to the source file
17971815
dest_path: Path to write the signed file to
1816+
signer: The signer to use
17981817
17991818
Returns:
1800-
A tuple of (size of C2PA data, optional manifest bytes)
1819+
Size of C2PA data
18011820
18021821
Raises:
18031822
C2paError: If there was an error during signing
18041823
"""
1805-
if not self._builder:
1806-
raise C2paError(self._error_messages['closed_error'])
1807-
1808-
source_path_str = str(source_path).encode('utf-8')
1809-
dest_path_str = str(dest_path).encode('utf-8')
1810-
manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)()
1824+
# Get the MIME type from the file extension
1825+
mime_type = mimetypes.guess_type(str(source_path))[0]
1826+
if not mime_type:
1827+
raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}")
18111828

1812-
result = _lib.c2pa_builder_sign_file(
1813-
self._builder,
1814-
source_path_str,
1815-
dest_path_str,
1816-
signer._signer,
1817-
ctypes.byref(manifest_bytes_ptr)
1818-
)
1819-
1820-
if result < 0:
1821-
error = _parse_operation_result_for_error(_lib.c2pa_error())
1822-
if error:
1823-
raise C2paError(error)
1824-
1825-
manifest_bytes = None
1826-
if manifest_bytes_ptr:
1827-
# Convert the manifest bytes to a Python bytes object
1828-
size = result
1829-
manifest_bytes = bytes(manifest_bytes_ptr[:size])
1830-
_lib.c2pa_manifest_bytes_free(manifest_bytes_ptr)
1829+
# Open source and destination files
1830+
with open(source_path, 'rb') as source_file, open(dest_path, 'wb') as dest_file:
1831+
# Convert Python streams to Stream objects
1832+
source_stream = Stream(source_file)
1833+
dest_stream = Stream(dest_file)
18311834

1832-
return result, manifest_bytes
1835+
# Use the internal stream-base signing logic
1836+
return self._sign_internal(signer, mime_type, source_stream, dest_stream)
18331837

18341838

18351839
def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]:

tests/test_unit_tests.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,13 @@ def test_remote_sign(self):
322322
builder = Builder(self.manifestDefinition)
323323
builder.set_no_embed()
324324
output = io.BytesIO(bytearray())
325-
manifest_data = builder.sign(
326-
self.signer, "image/jpeg", file, output)
325+
result_data = builder.sign(self.signer, "image/jpeg", file, output)
326+
327327
output.seek(0)
328-
reader = Reader("image/jpeg", output, manifest_data)
329-
json_data = reader.json()
330-
self.assertIn("Python Test", json_data)
331-
self.assertNotIn("validation_status", json_data)
328+
# When set_no_embed() is used, no manifest should be embedded in the file
329+
# So reading from the file should fail
330+
with self.assertRaises(Error):
331+
reader = Reader("image/jpeg", output)
332332
output.close()
333333

334334
def test_sign_all_files(self):
@@ -404,7 +404,7 @@ def test_builder_double_close(self):
404404
# Verify builder is closed
405405
with self.assertRaises(Error):
406406
builder.set_no_embed()
407-
407+
408408
def test_builder_add_ingredient_on_closed_builder(self):
409409
"""Test that exception is raised when trying to add ingredient after close."""
410410
builder = Builder(self.manifestDefinition)
@@ -709,12 +709,46 @@ def test_builder_set_remote_url_no_embed(self):
709709
output.seek(0)
710710
with self.assertRaises(Error) as e:
711711
Reader("image/jpeg", output)
712-
712+
713713
self.assertIn("http://this_does_not_exist/foo.jpg", e.exception.message)
714-
714+
715715
# Return back to default settings
716716
load_settings(r'{"verify": { "remote_manifest_fetch": true} }')
717-
717+
718+
def test_sign_file(self):
719+
"""Test signing a file using the sign_file method."""
720+
import tempfile
721+
import shutil
722+
723+
# Create a temporary directory for the test
724+
temp_dir = tempfile.mkdtemp()
725+
try:
726+
# Create a temporary output file path
727+
output_path = os.path.join(temp_dir, "signed_output.jpg")
728+
729+
# Use the sign_file method
730+
builder = Builder(self.manifestDefinition)
731+
result = builder.sign_file(
732+
source_path=self.testPath,
733+
dest_path=output_path,
734+
signer=self.signer
735+
)
736+
737+
# Verify the output file was created
738+
self.assertTrue(os.path.exists(output_path))
739+
740+
# Read the signed file and verify the manifest
741+
with open(output_path, "rb") as file:
742+
reader = Reader("image/jpeg", file)
743+
json_data = reader.json()
744+
self.assertIn("Python Test", json_data)
745+
self.assertNotIn("validation_status", json_data)
746+
747+
finally:
748+
# Clean up the temporary directory
749+
shutil.rmtree(temp_dir)
750+
751+
718752
class TestStream(unittest.TestCase):
719753
def setUp(self):
720754
# Create a temporary file for testing

tests/test_unit_tests_threaded.py

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,109 +1068,6 @@ def read_manifest(reader_id):
10681068
# Verify all readers completed
10691069
self.assertEqual(active_readers, 0, "Not all readers completed")
10701070

1071-
def test_remote_sign_threaded(self):
1072-
"""Test remote signing with multiple threads in parallel"""
1073-
output1 = io.BytesIO(bytearray())
1074-
output2 = io.BytesIO(bytearray())
1075-
sign_errors = []
1076-
sign_complete = threading.Event()
1077-
manifest_data1 = None
1078-
manifest_data2 = None
1079-
1080-
def remote_sign(output_stream, manifest_def, thread_id):
1081-
nonlocal manifest_data1, manifest_data2
1082-
try:
1083-
with open(self.testPath, "rb") as file:
1084-
builder = Builder(manifest_def)
1085-
builder.set_no_embed()
1086-
manifest_data = builder.sign(
1087-
self.signer, "image/jpeg", file, output_stream)
1088-
output_stream.seek(0)
1089-
1090-
# Store manifest data for final verification
1091-
if thread_id == 1:
1092-
manifest_data1 = manifest_data
1093-
else:
1094-
manifest_data2 = manifest_data
1095-
1096-
# Verify the signed file
1097-
reader = Reader("image/jpeg", output_stream, manifest_data)
1098-
json_data = reader.json()
1099-
manifest_store = json.loads(json_data)
1100-
active_manifest = manifest_store["manifests"][manifest_store["active_manifest"]]
1101-
1102-
# Verify the correct manifest was used
1103-
if thread_id == 1:
1104-
expected_claim_generator = "python_test_1/0.0.1"
1105-
expected_author = "Tester One"
1106-
else:
1107-
expected_claim_generator = "python_test_2/0.0.1"
1108-
expected_author = "Tester Two"
1109-
1110-
self.assertEqual(
1111-
active_manifest["claim_generator"],
1112-
expected_claim_generator)
1113-
1114-
# Verify the author is correct
1115-
assertions = active_manifest["assertions"]
1116-
for assertion in assertions:
1117-
if assertion["label"] == "com.unit.test":
1118-
author_name = assertion["data"]["author"][0]["name"]
1119-
self.assertEqual(author_name, expected_author)
1120-
break
1121-
1122-
except Exception as e:
1123-
sign_errors.append(f"Thread {thread_id} error: {str(e)}")
1124-
finally:
1125-
sign_complete.set()
1126-
1127-
# Create and start two threads for concurrent remote signing
1128-
thread1 = threading.Thread(
1129-
target=remote_sign,
1130-
args=(output1, self.manifestDefinition_1, 1)
1131-
)
1132-
thread2 = threading.Thread(
1133-
target=remote_sign,
1134-
args=(output2, self.manifestDefinition_2, 2)
1135-
)
1136-
1137-
# Start both threads
1138-
thread1.start()
1139-
thread2.start()
1140-
1141-
# Wait for both threads to complete
1142-
thread1.join()
1143-
thread2.join()
1144-
1145-
# Check for errors
1146-
if sign_errors:
1147-
self.fail("\n".join(sign_errors))
1148-
1149-
# Verify the outputs are different before closing
1150-
output1.seek(0)
1151-
output2.seek(0)
1152-
reader1 = Reader("image/jpeg", output1, manifest_data1)
1153-
reader2 = Reader("image/jpeg", output2, manifest_data2)
1154-
1155-
manifest_store1 = json.loads(reader1.json())
1156-
manifest_store2 = json.loads(reader2.json())
1157-
1158-
# Get the active manifests
1159-
active_manifest1 = manifest_store1["manifests"][manifest_store1["active_manifest"]]
1160-
active_manifest2 = manifest_store2["manifests"][manifest_store2["active_manifest"]]
1161-
1162-
# Verify the manifests are different
1163-
self.assertNotEqual(
1164-
active_manifest1["claim_generator"],
1165-
active_manifest2["claim_generator"])
1166-
self.assertNotEqual(
1167-
active_manifest1["title"],
1168-
active_manifest2["title"])
1169-
1170-
# Clean up after verification
1171-
output1.close()
1172-
output2.close()
1173-
11741071
def test_archive_sign_threaded(self):
11751072
"""Test archive signing with multiple threads in parallel"""
11761073
archive1 = io.BytesIO(bytearray())

0 commit comments

Comments
 (0)