From 23f44d646896149e33d09a3de94cf0fd6327ed65 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 13:18:19 -0700 Subject: [PATCH 1/8] fix: Missing symbol --- src/c2pa/c2pa.py | 92 ++++++++++++++++++++++------------------ tests/test_unit_tests.py | 42 ++++++++++++++++-- 2 files changed, 89 insertions(+), 45 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index d14288ac..a2724cbf 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -1725,33 +1725,29 @@ def to_archive(self, stream: Any): raise C2paError( self._error_messages['archive_error'].format("Unknown error")) - def sign( + def _sign_internal( self, signer: Signer, format: str, - source: Any, - dest: Any = None) -> Optional[bytes]: - """Sign the builder's content and write to a destination stream. - + source_stream: Stream, + dest_stream: Stream) -> tuple[int, Optional[bytes]]: + """Core signing logic shared between sign() and sign_file() methods. + Args: - format: The MIME type or extension of the content - source: The source stream (any Python stream-like object) - dest: The destination stream (any Python stream-like object) signer: The signer to use - + format: The MIME type or extension of the content + source_stream: The source stream + dest_stream: The destination stream + Returns: A tuple of (size of C2PA data, optional manifest bytes) - + Raises: C2paError: If there was an error during signing """ if not self._builder: raise C2paError(self._error_messages['closed_error']) - # Convert Python streams to Stream objects - source_stream = Stream(source) - dest_stream = Stream(dest) - try: format_str = format.encode('utf-8') manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)() @@ -1777,12 +1773,40 @@ def sign( manifest_bytes = bytes(manifest_bytes_ptr[:size]) _lib.c2pa_manifest_bytes_free(manifest_bytes_ptr) - return manifest_bytes + return result, manifest_bytes finally: # Ensure both streams are cleaned up source_stream.close() dest_stream.close() + def sign( + self, + signer: Signer, + format: str, + source: Any, + dest: Any = None) -> Optional[bytes]: + """Sign the builder's content and write to a destination stream. + + Args: + format: The MIME type or extension of the content + source: The source stream (any Python stream-like object) + dest: The destination stream (any Python stream-like object) + signer: The signer to use + + Returns: + The manifest bytes if available, None otherwise + + Raises: + C2paError: If there was an error during signing + """ + # Convert Python streams to Stream objects + source_stream = Stream(source) + dest_stream = Stream(dest) + + # Use the core signing logic + _, manifest_bytes = self._sign_internal(signer, format, source_stream, dest_stream) + return manifest_bytes + def sign_file(self, source_path: Union[str, Path], @@ -1795,6 +1819,7 @@ def sign_file(self, Args: source_path: Path to the source file dest_path: Path to write the signed file to + signer: The signer to use Returns: A tuple of (size of C2PA data, optional manifest bytes) @@ -1802,34 +1827,19 @@ def sign_file(self, Raises: C2paError: If there was an error during signing """ - if not self._builder: - raise C2paError(self._error_messages['closed_error']) - - source_path_str = str(source_path).encode('utf-8') - dest_path_str = str(dest_path).encode('utf-8') - manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)() - - result = _lib.c2pa_builder_sign_file( - self._builder, - source_path_str, - dest_path_str, - signer._signer, - ctypes.byref(manifest_bytes_ptr) - ) + # Get the MIME type from the file extension + mime_type = mimetypes.guess_type(str(source_path))[0] + if not mime_type: + raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}") - if result < 0: - error = _parse_operation_result_for_error(_lib.c2pa_error()) - if error: - raise C2paError(error) - - manifest_bytes = None - if manifest_bytes_ptr: - # Convert the manifest bytes to a Python bytes object - size = result - manifest_bytes = bytes(manifest_bytes_ptr[:size]) - _lib.c2pa_manifest_bytes_free(manifest_bytes_ptr) + # Open source and destination files + with open(source_path, 'rb') as source_file, open(dest_path, 'wb') as dest_file: + # Convert Python streams to Stream objects + source_stream = Stream(source_file) + dest_stream = Stream(dest_file) - return result, manifest_bytes + # Use the core signing logic + return self._sign_internal(signer, mime_type, source_stream, dest_stream) def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]: diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 46247088..4b49c8d7 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -404,7 +404,7 @@ def test_builder_double_close(self): # Verify builder is closed with self.assertRaises(Error): builder.set_no_embed() - + def test_builder_add_ingredient_on_closed_builder(self): """Test that exception is raised when trying to add ingredient after close.""" builder = Builder(self.manifestDefinition) @@ -709,12 +709,46 @@ def test_builder_set_remote_url_no_embed(self): output.seek(0) with self.assertRaises(Error) as e: Reader("image/jpeg", output) - + self.assertIn("http://this_does_not_exist/foo.jpg", e.exception.message) - + # Return back to default settings load_settings(r'{"verify": { "remote_manifest_fetch": true} }') - + + def test_sign_file(self): + """Test signing a file using the sign_file method.""" + import tempfile + import shutil + + # Create a temporary directory for the test + temp_dir = tempfile.mkdtemp() + try: + # Create a temporary output file path + output_path = os.path.join(temp_dir, "signed_output.jpg") + + # Use the sign_file method + builder = Builder(self.manifestDefinition) + result, manifest_bytes = builder.sign_file( + source_path=self.testPath, + dest_path=output_path, + signer=self.signer + ) + + # Verify the output file was created + self.assertTrue(os.path.exists(output_path)) + + # Read the signed file and verify the manifest + with open(output_path, "rb") as file: + reader = Reader("image/jpeg", file) + json_data = reader.json() + self.assertIn("Python Test", json_data) + self.assertNotIn("validation_status", json_data) + + finally: + # Clean up the temporary directory + shutil.rmtree(temp_dir) + + class TestStream(unittest.TestCase): def setUp(self): # Create a temporary file for testing From 9d40c79faaf83f69c4e9b894a4a2e57f8172e026 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 13:20:32 -0700 Subject: [PATCH 2/8] fix: Refactor --- src/c2pa/c2pa.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index a2724cbf..0bb1e4bd 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -1731,17 +1731,18 @@ def _sign_internal( format: str, source_stream: Stream, dest_stream: Stream) -> tuple[int, Optional[bytes]]: - """Core signing logic shared between sign() and sign_file() methods. - + """Core signing logic shared between sign() and sign_file() methods, + to use same native calls but expose different API surface. + Args: signer: The signer to use format: The MIME type or extension of the content source_stream: The source stream dest_stream: The destination stream - + Returns: A tuple of (size of C2PA data, optional manifest bytes) - + Raises: C2paError: If there was an error during signing """ From 53cc06d0d6871ccb0d35507fd5cbdb0099a7aaff Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 13:25:55 -0700 Subject: [PATCH 3/8] fix: Docs --- src/c2pa/c2pa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 0bb1e4bd..afbf6d80 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -1731,7 +1731,7 @@ def _sign_internal( format: str, source_stream: Stream, dest_stream: Stream) -> tuple[int, Optional[bytes]]: - """Core signing logic shared between sign() and sign_file() methods, + """Internal signing logic shared between sign() and sign_file() methods, to use same native calls but expose different API surface. Args: @@ -1804,7 +1804,7 @@ def sign( source_stream = Stream(source) dest_stream = Stream(dest) - # Use the core signing logic + # Use the internal stream-base signing logic _, manifest_bytes = self._sign_internal(signer, format, source_stream, dest_stream) return manifest_bytes @@ -1839,7 +1839,7 @@ def sign_file(self, source_stream = Stream(source_file) dest_stream = Stream(dest_file) - # Use the core signing logic + # Use the internal stream-base signing logic return self._sign_internal(signer, mime_type, source_stream, dest_stream) From f9c977c28a1e26098e61aa5cf0b75625e5b87c77 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 13:26:33 -0700 Subject: [PATCH 4/8] fix: Format --- src/c2pa/c2pa.py | 21 ++++++++++++--------- src/c2pa/lib.py | 27 ++++++++++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index afbf6d80..c79e4002 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -581,8 +581,7 @@ def read_file(path: Union[str, Path], "The read_file function is deprecated and will be removed in a future version. " "Please use the Reader class for reading C2PA metadata instead.", DeprecationWarning, - stacklevel=2 - ) + stacklevel=2) container = _StringContainer() @@ -626,8 +625,7 @@ def sign_file( "The sign_file function is deprecated and will be removed in a future version. " "Please use the Builder class for signing and the Reader class for reading signed data instead.", DeprecationWarning, - stacklevel=2 - ) + stacklevel=2) try: # Create a signer from the signer info @@ -641,7 +639,8 @@ def sign_file( # Get the MIME type from the file extension mime_type = mimetypes.guess_type(str(source_path))[0] if not mime_type: - raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}") + raise C2paError.NotSupported( + f"Could not determine MIME type for file: {source_path}") # Sign the file using the builder manifest_bytes = builder.sign( @@ -701,7 +700,8 @@ def __init__(self, file): Raises: TypeError: If the file object doesn't implement all required methods """ - # Initialize _closed first to prevent AttributeError during garbage collection + # Initialize _closed first to prevent AttributeError during garbage + # collection self._closed = False self._initialized = False self._stream = None @@ -1805,7 +1805,8 @@ def sign( dest_stream = Stream(dest) # Use the internal stream-base signing logic - _, manifest_bytes = self._sign_internal(signer, format, source_stream, dest_stream) + _, manifest_bytes = self._sign_internal( + signer, format, source_stream, dest_stream) return manifest_bytes def sign_file(self, @@ -1831,7 +1832,8 @@ def sign_file(self, # Get the MIME type from the file extension mime_type = mimetypes.guess_type(str(source_path))[0] if not mime_type: - raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}") + raise C2paError.NotSupported( + f"Could not determine MIME type for file: {source_path}") # Open source and destination files with open(source_path, 'rb') as source_file, open(dest_path, 'wb') as dest_file: @@ -1840,7 +1842,8 @@ def sign_file(self, dest_stream = Stream(dest_file) # Use the internal stream-base signing logic - return self._sign_internal(signer, mime_type, source_stream, dest_stream) + return self._sign_internal( + signer, mime_type, source_stream, dest_stream) def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]: diff --git a/src/c2pa/lib.py b/src/c2pa/lib.py index f0382874..0a9546d1 100644 --- a/src/c2pa/lib.py +++ b/src/c2pa/lib.py @@ -122,7 +122,8 @@ def _load_single_library(lib_name: str, The loaded library or None if loading failed """ if DEBUG_LIBRARY_LOADING: - logger.info(f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}") + logger.info(f"Searching for library '{lib_name}' in paths: { + [str(p) for p in search_paths]}") current_arch = _get_architecture() if DEBUG_LIBRARY_LOADING: logger.info(f"Current architecture: {current_arch}") @@ -139,10 +140,12 @@ def _load_single_library(lib_name: str, except Exception as e: error_msg = str(e) if "incompatible architecture" in error_msg: - logger.error(f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}") + logger.error(f"Architecture mismatch: Library at { + lib_path} is not compatible with current architecture {current_arch}") logger.error(f"Error details: {error_msg}") else: - logger.error(f"Failed to load library from {lib_path}: {e}") + logger.error( + f"Failed to load library from {lib_path}: {e}") else: logger.debug(f"Library not found at: {lib_path}") return None @@ -233,7 +236,8 @@ def dynamically_load_library( if lib: return lib else: - logger.error(f"Could not find library {env_lib_name} in any of the search paths") + logger.error(f"Could not find library { + env_lib_name} in any of the search paths") # Continue with normal loading if environment variable library # name fails except Exception as e: @@ -249,15 +253,20 @@ def dynamically_load_library( if not lib: platform_id = get_platform_identifier() current_arch = _get_architecture() - logger.error(f"Could not find {lib_name} in any of the search paths: {[str(p) for p in possible_paths]}") - logger.error(f"Platform: {platform_id}, Architecture: {current_arch}") - raise RuntimeError(f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})") + logger.error(f"Could not find {lib_name} in any of the search paths: { + [str(p) for p in possible_paths]}") + logger.error( + f"Platform: {platform_id}, Architecture: {current_arch}") + raise RuntimeError(f"Could not find {lib_name} in any of the search paths (Platform: { + platform_id}, Architecture: {current_arch})") return lib # Default path (no library name provided in the environment) c2pa_lib = _load_single_library(c2pa_lib_name, possible_paths) if not c2pa_lib: - logger.error(f"Could not find {c2pa_lib_name} in any of the search paths: {[str(p) for p in possible_paths]}") - raise RuntimeError(f"Could not find {c2pa_lib_name} in any of the search paths") + logger.error(f"Could not find {c2pa_lib_name} in any of the search paths: { + [str(p) for p in possible_paths]}") + raise RuntimeError( + f"Could not find {c2pa_lib_name} in any of the search paths") return c2pa_lib From bd580a402d7f708e536e04359512a255d446ed46 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 13:29:08 -0700 Subject: [PATCH 5/8] ci: Revert "fix: Format - tests do not like format change " This reverts commit f9c977c28a1e26098e61aa5cf0b75625e5b87c77. --- src/c2pa/c2pa.py | 21 +++++++++------------ src/c2pa/lib.py | 27 +++++++++------------------ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index c79e4002..afbf6d80 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -581,7 +581,8 @@ def read_file(path: Union[str, Path], "The read_file function is deprecated and will be removed in a future version. " "Please use the Reader class for reading C2PA metadata instead.", DeprecationWarning, - stacklevel=2) + stacklevel=2 + ) container = _StringContainer() @@ -625,7 +626,8 @@ def sign_file( "The sign_file function is deprecated and will be removed in a future version. " "Please use the Builder class for signing and the Reader class for reading signed data instead.", DeprecationWarning, - stacklevel=2) + stacklevel=2 + ) try: # Create a signer from the signer info @@ -639,8 +641,7 @@ def sign_file( # Get the MIME type from the file extension mime_type = mimetypes.guess_type(str(source_path))[0] if not mime_type: - raise C2paError.NotSupported( - f"Could not determine MIME type for file: {source_path}") + raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}") # Sign the file using the builder manifest_bytes = builder.sign( @@ -700,8 +701,7 @@ def __init__(self, file): Raises: TypeError: If the file object doesn't implement all required methods """ - # Initialize _closed first to prevent AttributeError during garbage - # collection + # Initialize _closed first to prevent AttributeError during garbage collection self._closed = False self._initialized = False self._stream = None @@ -1805,8 +1805,7 @@ def sign( dest_stream = Stream(dest) # Use the internal stream-base signing logic - _, manifest_bytes = self._sign_internal( - signer, format, source_stream, dest_stream) + _, manifest_bytes = self._sign_internal(signer, format, source_stream, dest_stream) return manifest_bytes def sign_file(self, @@ -1832,8 +1831,7 @@ def sign_file(self, # Get the MIME type from the file extension mime_type = mimetypes.guess_type(str(source_path))[0] if not mime_type: - raise C2paError.NotSupported( - f"Could not determine MIME type for file: {source_path}") + raise C2paError.NotSupported(f"Could not determine MIME type for file: {source_path}") # Open source and destination files with open(source_path, 'rb') as source_file, open(dest_path, 'wb') as dest_file: @@ -1842,8 +1840,7 @@ def sign_file(self, dest_stream = Stream(dest_file) # Use the internal stream-base signing logic - return self._sign_internal( - signer, mime_type, source_stream, dest_stream) + return self._sign_internal(signer, mime_type, source_stream, dest_stream) def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]: diff --git a/src/c2pa/lib.py b/src/c2pa/lib.py index 0a9546d1..f0382874 100644 --- a/src/c2pa/lib.py +++ b/src/c2pa/lib.py @@ -122,8 +122,7 @@ def _load_single_library(lib_name: str, The loaded library or None if loading failed """ if DEBUG_LIBRARY_LOADING: - logger.info(f"Searching for library '{lib_name}' in paths: { - [str(p) for p in search_paths]}") + logger.info(f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}") current_arch = _get_architecture() if DEBUG_LIBRARY_LOADING: logger.info(f"Current architecture: {current_arch}") @@ -140,12 +139,10 @@ def _load_single_library(lib_name: str, except Exception as e: error_msg = str(e) if "incompatible architecture" in error_msg: - logger.error(f"Architecture mismatch: Library at { - lib_path} is not compatible with current architecture {current_arch}") + logger.error(f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}") logger.error(f"Error details: {error_msg}") else: - logger.error( - f"Failed to load library from {lib_path}: {e}") + logger.error(f"Failed to load library from {lib_path}: {e}") else: logger.debug(f"Library not found at: {lib_path}") return None @@ -236,8 +233,7 @@ def dynamically_load_library( if lib: return lib else: - logger.error(f"Could not find library { - env_lib_name} in any of the search paths") + logger.error(f"Could not find library {env_lib_name} in any of the search paths") # Continue with normal loading if environment variable library # name fails except Exception as e: @@ -253,20 +249,15 @@ def dynamically_load_library( if not lib: platform_id = get_platform_identifier() current_arch = _get_architecture() - logger.error(f"Could not find {lib_name} in any of the search paths: { - [str(p) for p in possible_paths]}") - logger.error( - f"Platform: {platform_id}, Architecture: {current_arch}") - raise RuntimeError(f"Could not find {lib_name} in any of the search paths (Platform: { - platform_id}, Architecture: {current_arch})") + logger.error(f"Could not find {lib_name} in any of the search paths: {[str(p) for p in possible_paths]}") + logger.error(f"Platform: {platform_id}, Architecture: {current_arch}") + raise RuntimeError(f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})") return lib # Default path (no library name provided in the environment) c2pa_lib = _load_single_library(c2pa_lib_name, possible_paths) if not c2pa_lib: - logger.error(f"Could not find {c2pa_lib_name} in any of the search paths: { - [str(p) for p in possible_paths]}") - raise RuntimeError( - f"Could not find {c2pa_lib_name} in any of the search paths") + logger.error(f"Could not find {c2pa_lib_name} in any of the search paths: {[str(p) for p in possible_paths]}") + raise RuntimeError(f"Could not find {c2pa_lib_name} in any of the search paths") return c2pa_lib From b5bd2b97c447aad0e5f3522f93a2e768239c299a Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 13:43:56 -0700 Subject: [PATCH 6/8] fix: Pipeline trigger --- src/c2pa/c2pa.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index afbf6d80..f79a4786 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -1753,6 +1753,7 @@ def _sign_internal( format_str = format.encode('utf-8') manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)() + # c2pa_builder_sign uses streams result = _lib.c2pa_builder_sign( self._builder, format_str, From d86503897c75fdcf0c772031459ff86d418a540f Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 15:44:02 -0700 Subject: [PATCH 7/8] fix: One more signature change --- src/c2pa/c2pa.py | 24 +++---- tests/test_unit_tests.py | 14 ++-- tests/test_unit_tests_threaded.py | 103 ------------------------------ 3 files changed, 15 insertions(+), 126 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index f79a4786..40c15316 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -1730,7 +1730,7 @@ def _sign_internal( signer: Signer, format: str, source_stream: Stream, - dest_stream: Stream) -> tuple[int, Optional[bytes]]: + dest_stream: Stream) -> int: """Internal signing logic shared between sign() and sign_file() methods, to use same native calls but expose different API surface. @@ -1741,7 +1741,7 @@ def _sign_internal( dest_stream: The destination stream Returns: - A tuple of (size of C2PA data, optional manifest bytes) + Size of C2PA data Raises: C2paError: If there was an error during signing @@ -1768,14 +1768,11 @@ def _sign_internal( if error: raise C2paError(error) - manifest_bytes = None if manifest_bytes_ptr: - # Convert the manifest bytes to a Python bytes object - size = result - manifest_bytes = bytes(manifest_bytes_ptr[:size]) + # Free the manifest bytes pointer if it was allocated _lib.c2pa_manifest_bytes_free(manifest_bytes_ptr) - return result, manifest_bytes + return result finally: # Ensure both streams are cleaned up source_stream.close() @@ -1786,7 +1783,7 @@ def sign( signer: Signer, format: str, source: Any, - dest: Any = None) -> Optional[bytes]: + dest: Any = None) -> None: """Sign the builder's content and write to a destination stream. Args: @@ -1795,9 +1792,6 @@ def sign( dest: The destination stream (any Python stream-like object) signer: The signer to use - Returns: - The manifest bytes if available, None otherwise - Raises: C2paError: If there was an error during signing """ @@ -1806,16 +1800,14 @@ def sign( dest_stream = Stream(dest) # Use the internal stream-base signing logic - _, manifest_bytes = self._sign_internal(signer, format, source_stream, dest_stream) - return manifest_bytes + self._sign_internal(signer, format, source_stream, dest_stream) def sign_file(self, source_path: Union[str, Path], dest_path: Union[str, Path], - signer: Signer) -> tuple[int, - Optional[bytes]]: + signer: Signer) -> int: """Sign a file and write the signed data to an output file. Args: @@ -1824,7 +1816,7 @@ def sign_file(self, signer: The signer to use Returns: - A tuple of (size of C2PA data, optional manifest bytes) + Size of C2PA data Raises: C2paError: If there was an error during signing diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 4b49c8d7..a2a07870 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -322,13 +322,13 @@ def test_remote_sign(self): builder = Builder(self.manifestDefinition) builder.set_no_embed() output = io.BytesIO(bytearray()) - manifest_data = builder.sign( - self.signer, "image/jpeg", file, output) + result_data = builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) - reader = Reader("image/jpeg", output, manifest_data) - json_data = reader.json() - self.assertIn("Python Test", json_data) - self.assertNotIn("validation_status", json_data) + # When set_no_embed() is used, no manifest should be embedded in the file + # So reading from the file should fail + with self.assertRaises(Error): + reader = Reader("image/jpeg", output) output.close() def test_sign_all_files(self): @@ -728,7 +728,7 @@ def test_sign_file(self): # Use the sign_file method builder = Builder(self.manifestDefinition) - result, manifest_bytes = builder.sign_file( + result = builder.sign_file( source_path=self.testPath, dest_path=output_path, signer=self.signer diff --git a/tests/test_unit_tests_threaded.py b/tests/test_unit_tests_threaded.py index a9821dcb..f1b61331 100644 --- a/tests/test_unit_tests_threaded.py +++ b/tests/test_unit_tests_threaded.py @@ -1068,109 +1068,6 @@ def read_manifest(reader_id): # Verify all readers completed self.assertEqual(active_readers, 0, "Not all readers completed") - def test_remote_sign_threaded(self): - """Test remote signing with multiple threads in parallel""" - output1 = io.BytesIO(bytearray()) - output2 = io.BytesIO(bytearray()) - sign_errors = [] - sign_complete = threading.Event() - manifest_data1 = None - manifest_data2 = None - - def remote_sign(output_stream, manifest_def, thread_id): - nonlocal manifest_data1, manifest_data2 - try: - with open(self.testPath, "rb") as file: - builder = Builder(manifest_def) - builder.set_no_embed() - manifest_data = builder.sign( - self.signer, "image/jpeg", file, output_stream) - output_stream.seek(0) - - # Store manifest data for final verification - if thread_id == 1: - manifest_data1 = manifest_data - else: - manifest_data2 = manifest_data - - # Verify the signed file - reader = Reader("image/jpeg", output_stream, manifest_data) - json_data = reader.json() - manifest_store = json.loads(json_data) - active_manifest = manifest_store["manifests"][manifest_store["active_manifest"]] - - # Verify the correct manifest was used - if thread_id == 1: - expected_claim_generator = "python_test_1/0.0.1" - expected_author = "Tester One" - else: - expected_claim_generator = "python_test_2/0.0.1" - expected_author = "Tester Two" - - self.assertEqual( - active_manifest["claim_generator"], - expected_claim_generator) - - # Verify the author is correct - assertions = active_manifest["assertions"] - for assertion in assertions: - if assertion["label"] == "com.unit.test": - author_name = assertion["data"]["author"][0]["name"] - self.assertEqual(author_name, expected_author) - break - - except Exception as e: - sign_errors.append(f"Thread {thread_id} error: {str(e)}") - finally: - sign_complete.set() - - # Create and start two threads for concurrent remote signing - thread1 = threading.Thread( - target=remote_sign, - args=(output1, self.manifestDefinition_1, 1) - ) - thread2 = threading.Thread( - target=remote_sign, - args=(output2, self.manifestDefinition_2, 2) - ) - - # Start both threads - thread1.start() - thread2.start() - - # Wait for both threads to complete - thread1.join() - thread2.join() - - # Check for errors - if sign_errors: - self.fail("\n".join(sign_errors)) - - # Verify the outputs are different before closing - output1.seek(0) - output2.seek(0) - reader1 = Reader("image/jpeg", output1, manifest_data1) - reader2 = Reader("image/jpeg", output2, manifest_data2) - - manifest_store1 = json.loads(reader1.json()) - manifest_store2 = json.loads(reader2.json()) - - # Get the active manifests - active_manifest1 = manifest_store1["manifests"][manifest_store1["active_manifest"]] - active_manifest2 = manifest_store2["manifests"][manifest_store2["active_manifest"]] - - # Verify the manifests are different - self.assertNotEqual( - active_manifest1["claim_generator"], - active_manifest2["claim_generator"]) - self.assertNotEqual( - active_manifest1["title"], - active_manifest2["title"]) - - # Clean up after verification - output1.close() - output2.close() - def test_archive_sign_threaded(self): """Test archive signing with multiple threads in parallel""" archive1 = io.BytesIO(bytearray()) From 72cc96c05d75bd2887aa3a2bd1c80c6ba32f9632 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Jun 2025 15:55:58 -0700 Subject: [PATCH 8/8] fix: Bump version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e2149481..afa30d5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "c2pa-python" -version = "0.11.0" +version = "0.11.1" requires-python = ">=3.10" description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library" readme = { file = "README.md", content-type = "text/markdown" }