diff --git a/Makefile b/Makefile index 4070ff0e..06afe998 100644 --- a/Makefile +++ b/Makefile @@ -139,7 +139,7 @@ build-from-source: # Build API documentation with Sphinx docs: - python3 scripts/generate_api_docs.py + $(PYTHON) scripts/generate_api_docs.py # Memory profiling with memray (runs in Docker, reports go to tests/perf/reports/) # More details for usage are in tests/perf/README.md diff --git a/docs/release-notes.md b/docs/release-notes.md index 24c4048a..31ff90a8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,26 @@ # Release notes +## Version 0.33.0 + +### Breaking changes + +Removed the deprecated file-based free functions `read_file`, +`read_ingredient_file`, and `sign_file`, following the removal of the +underlying `c2pa_read_file`, `c2pa_read_ingredient_file`, and `c2pa_sign_file` +functions from the c2pa-rs C FFI. The deprecated `Builder.add_ingredient_from_file_path` +method was removed as well. + +Migration: + +- Instead of `read_file(path, data_dir)`, use `Reader(path).json()`. +- Instead of `read_ingredient_file(path, data_dir)`, use + `Builder.add_ingredient(json, format, stream)` to add the ingredient directly. +- Instead of the `sign_file(...)` free function, use the `Builder.sign_file(source_path, dest_path, signer)` method (or `Builder.sign(...)` for stream-based signing). +- Instead of `Builder.add_ingredient_from_file_path(json, format, filepath)`, open the + file and call `Builder.add_ingredient_from_stream(json, format, stream)`. + +The `Builder.sign_file` method and `load_settings` are unaffected and remain available. + ## Version 0.6.0 diff --git a/src/c2pa/__init__.py b/src/c2pa/__init__.py index 5a5bfe78..bd581d58 100644 --- a/src/c2pa/__init__.py +++ b/src/c2pa/__init__.py @@ -32,7 +32,6 @@ ContextBuilder, ContextProvider, sdk_version, - read_ingredient_file, load_settings ) # NOQA @@ -52,6 +51,5 @@ 'ContextBuilder', 'ContextProvider', 'sdk_version', - 'read_ingredient_file', 'load_settings' ] diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index ae8b307e..fc62632e 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -38,8 +38,6 @@ 'c2pa_error', # Legacy APIs, deprecated 'c2pa_load_settings', - 'c2pa_read_file', - 'c2pa_read_ingredient_file', # Stream 'c2pa_create_stream', 'c2pa_release_stream', @@ -645,12 +643,6 @@ def _setup_function(func, argtypes, restype=None): _setup_function(_lib.c2pa_signer_from_info, [ctypes.POINTER(C2paSignerInfo)], ctypes.POINTER(C2paSigner)) -_setup_function( - _lib.c2pa_read_file, [ - ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p) -_setup_function( - _lib.c2pa_read_ingredient_file, [ - ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p) # Set up Signer function prototypes _setup_function( @@ -1191,208 +1183,6 @@ def _get_mime_type_from_path(path: Union[str, Path]) -> str: return mime_type -def read_ingredient_file( - path: Union[str, Path], data_dir: Union[str, Path]) -> str: - """Read a file as C2PA ingredient (deprecated). - This creates the JSON string that would be used as the ingredient JSON. - - .. deprecated:: 0.11.0 - This function is deprecated and will be removed in a future version. - To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class. - To add ingredients to a manifest, - use :meth:`c2pa.c2pa.Builder.add_ingredient` instead. - - Args: - path: Path to the file to read - data_dir: Directory to write binary resources to - - Returns: - The ingredient as a JSON string - - Raises: - C2paError: If there was an error reading the file - """ - warnings.warn( - "The read_ingredient_file function is deprecated and will be " - "removed in a future version. Please use Reader(path).json() for " - "reading C2PA metadata instead, or " - "Builder.add_ingredient(json, format, stream) to add ingredients " - "to a manifest.", - DeprecationWarning, - stacklevel=2, - ) - - _clear_error_state() - - container = _StringContainer() - - container._path_str = str(path).encode('utf-8') - container._data_dir_str = str(data_dir).encode('utf-8') - - result = _lib.c2pa_read_ingredient_file( - container._path_str, container._data_dir_str) - - _check_ffi_operation_result( - result, "Error reading ingredient file {}".format(path)) - - return _convert_to_py_string(result) - - -def read_file(path: Union[str, Path], - data_dir: Union[str, Path]) -> str: - """Read a C2PA manifest from a file (deprecated). - - .. deprecated:: 0.10.0 - This function is deprecated and will be removed in a future version. - To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class. - - Args: - path: Path to the file to read - data_dir: Directory to write binary resources to - - Returns: - The manifest as a JSON string - - Raises: - C2paError: If there was an error reading the file - """ - warnings.warn( - "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, - ) - - _clear_error_state() - - container = _StringContainer() - - container._path_str = str(path).encode('utf-8') - container._data_dir_str = str(data_dir).encode('utf-8') - - result = _lib.c2pa_read_file(container._path_str, container._data_dir_str) - _check_ffi_operation_result( - result, "Error during read of manifest from file {}".format(path)) - - return _convert_to_py_string(result) - - -@overload -def sign_file( - source_path: Union[str, Path], - dest_path: Union[str, Path], - manifest: str, - signer_info: C2paSignerInfo, - return_manifest_as_bytes: bool = False -) -> Union[str, bytes]: - """Sign a file with a C2PA manifest using signer info. - """ - ... - - -@overload -def sign_file( - source_path: Union[str, Path], - dest_path: Union[str, Path], - manifest: str, - signer: 'Signer', - return_manifest_as_bytes: bool = False -) -> Union[str, bytes]: - """Sign a file with a C2PA manifest using a signer. - """ - ... - - -def sign_file( - source_path: Union[str, Path], - dest_path: Union[str, Path], - manifest: str, - signer_or_info: Union[C2paSignerInfo, 'Signer'], - return_manifest_as_bytes: bool = False -) -> Union[str, bytes]: - """Sign a file with a C2PA manifest (deprecated). - For now, this function is left here to provide a backwards-compatible API. - - .. deprecated:: 0.13.0 - This function is deprecated and will be removed in a future version. - Use :meth:`Builder.sign` instead. - - Args: - source_path: Path to the source file. We will attempt - to guess the mimetype of the source file based on - the extension. - dest_path: Path to write the signed file to - manifest: The manifest JSON string - signer_or_info: Either a signer configuration or a signer object - return_manifest_as_bytes: If True, return manifest bytes instead - of JSON string - - Returns: - The signed manifest as a JSON string or bytes, depending - on return_manifest_as_bytes - - Raises: - C2paError: If there was an error signing the file - C2paError.Encoding: If any of the string inputs contain - invalid UTF-8 characters - C2paError.NotSupported: If the file type cannot be determined - """ - - warnings.warn( - "The sign_file function is deprecated and will be removed in a " - "future version. Please use the Builder object and Builder.sign() " - "instead.", - DeprecationWarning, - stacklevel=2, - ) - - _clear_error_state() - - try: - # Determine if we have a signer or signer info - if isinstance(signer_or_info, C2paSignerInfo): - signer = Signer.from_info(signer_or_info) - own_signer = True - else: - signer = signer_or_info - own_signer = False - - # Create a builder from the manifest - builder = Builder(manifest) - - manifest_bytes = builder.sign_file( - source_path, - dest_path, - signer - ) - - if return_manifest_as_bytes: - return manifest_bytes - else: - # Read the signed manifest from the destination file - with Reader(dest_path) as reader: - return reader.json() - - except Exception as e: - # Clean up destination file if it exists and there was an error - if os.path.exists(dest_path): - try: - os.remove(dest_path) - except OSError: - logger.warning("Failed to remove destination file") - pass # Ignore cleanup errors - - # Re-raise the error - raise C2paError(f"Error signing file: {str(e)}") from e - finally: - # Ensure resources are cleaned up - if 'builder' in locals(): - builder.close() - if 'signer' in locals() and own_signer: - signer.close() - - class ContextProvider(ABC): """Abstract base class for types that provide a C2PA context. @@ -3411,52 +3201,6 @@ def add_ingredient_from_stream( Builder._ERROR_MESSAGES['ingredient_error'].format("Unknown error"), check=lambda r: r != 0) - def add_ingredient_from_file_path( - self, - ingredient_json: Union[str, dict], - format: str, - filepath: Union[str, Path]): - """Add an ingredient from a file path to the builder (deprecated). - This is a legacy method. - - .. deprecated:: 0.13.0 - This method is deprecated and will be removed in a future version. - Use :meth:`add_ingredient` with a file stream instead. - - Args: - ingredient_json: The JSON ingredient definition - (either a JSON string or a dictionary) - format: The MIME type or extension of the ingredient - filepath: The path to the file containing the ingredient data - (can be a string or Path object) - - Raises: - C2paError: If there was an error adding the ingredient - C2paError.Encoding: If the ingredient JSON or format - contains invalid UTF-8 characters - FileNotFoundError: If the file at the specified path does not exist - """ - warnings.warn( - "add_ingredient_from_file_path is deprecated and will " - "be removed in a future version. Use add_ingredient " - "with a file stream instead.", - DeprecationWarning, - stacklevel=2, - ) - - try: - # Convert Path object to string if necessary - filepath_str = str(filepath) - - # Does the stream handling to use add_ingredient_from_stream - with open(filepath_str, 'rb') as file_stream: - self.add_ingredient_from_stream( - ingredient_json, format, file_stream) - except FileNotFoundError as e: - raise C2paError.FileNotFound(f"File not found: {filepath}") from e - except Exception as e: - raise C2paError.Other(f"Could not add ingredient: {e}") from e - def add_action(self, action_json: Union[str, dict]) -> None: """Add an action to the builder, that will be placed in the actions assertion array in the generated manifest. @@ -3991,9 +3735,6 @@ def ed25519_sign(data: bytes, private_key: str) -> bytes: 'Builder', 'Signer', 'load_settings', - 'read_file', - 'read_ingredient_file', - 'sign_file', 'format_embeddable', 'version', 'sdk_version' diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index ecdd1bc6..8b40f981 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -31,7 +31,7 @@ from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version, C2paBuilderIntent, C2paDigitalSourceType from c2pa import Settings, Context, ContextBuilder, ContextProvider -from c2pa.c2pa import Stream, LifecycleState, read_ingredient_file, read_file, sign_file, load_settings, create_signer, create_signer_from_info, ed25519_sign, format_embeddable +from c2pa.c2pa import Stream, LifecycleState, load_settings, create_signer, create_signer_from_info, ed25519_sign, format_embeddable PROJECT_PATH = os.getcwd() @@ -4787,9 +4787,6 @@ def test_stream_flush_after_close(self): class TestLegacyAPI(unittest.TestCase): def setUp(self): # Filter specific deprecation warnings for legacy API tests - warnings.filterwarnings("ignore", message="The read_file function is deprecated") - warnings.filterwarnings("ignore", message="The sign_file function is deprecated") - warnings.filterwarnings("ignore", message="The read_ingredient_file function is deprecated") warnings.filterwarnings("ignore", message="The create_signer function is deprecated") warnings.filterwarnings("ignore", message="The create_signer_from_info function is deprecated") warnings.filterwarnings("ignore", message="load_settings\\(\\) is deprecated") @@ -4863,489 +4860,6 @@ def tearDown(self): if os.path.exists(self.temp_data_dir): shutil.rmtree(self.temp_data_dir) - def test_read_ingredient_file(self): - """Test reading a C2PA ingredient from a file.""" - # Test reading ingredient from file with data_dir - temp_data_dir = os.path.join(self.data_dir, "temp_data") - os.makedirs(temp_data_dir, exist_ok=True) - - ingredient_json_with_dir = read_ingredient_file(self.testPath, temp_data_dir) - - # Verify some fields - ingredient_data = json.loads(ingredient_json_with_dir) - self.assertEqual(ingredient_data["title"], DEFAULT_TEST_FILE_NAME) - self.assertEqual(ingredient_data["format"], "image/jpeg") - self.assertIn("thumbnail", ingredient_data) - - def test_read_ingredient_file_who_has_no_manifest(self): - """Test reading a C2PA ingredient from a file.""" - # Test reading ingredient from file with data_dir - temp_data_dir = os.path.join(self.data_dir, "temp_data") - os.makedirs(temp_data_dir, exist_ok=True) - - # Load settings first, before they need to be used - load_settings('{"builder": { "thumbnail": {"enabled": false}}}') - - ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir) - - # Verify some fields - ingredient_data = json.loads(ingredient_json_with_dir) - self.assertEqual(ingredient_data["title"], INGREDIENT_TEST_FILE_NAME) - self.assertEqual(ingredient_data["format"], "image/jpeg") - self.assertNotIn("thumbnail", ingredient_data) - - # Reset setting - load_settings('{"builder": { "thumbnail": {"enabled": true}}}') - - def test_compare_read_ingredient_file_with_builder_added_ingredient(self): - """Test reading a C2PA ingredient from a file.""" - # Test reading ingredient from file with data_dir - temp_data_dir = os.path.join(self.data_dir, "temp_data") - os.makedirs(temp_data_dir, exist_ok=True) - - ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir) - - # Ingredient fields from read_ingredient_file - ingredient_data = json.loads(ingredient_json_with_dir) - - # Compare with ingredient added by Builder - builder = Builder.from_json(self.manifestDefinition) - # Only the title is needed (filename), since title not extracted or guessed from filename - ingredient_json = '{ "title" : "A.jpg" }' - with open(self.testPath2, 'rb') as f: - builder.add_ingredient(ingredient_json, "image/jpeg", f) - - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - - # Get ingredient fields from signed manifest - reader = Reader("image/jpeg", output) - json_data = reader.json() - manifest_data = json.loads(json_data) - active_manifest_id = manifest_data["active_manifest"] - active_manifest = manifest_data["manifests"][active_manifest_id] - only_ingredient = active_manifest["ingredients"][0] - - self.assertEqual(ingredient_data["title"], only_ingredient["title"]) - self.assertEqual(ingredient_data["format"], only_ingredient["format"]) - self.assertEqual(ingredient_data["document_id"], only_ingredient["document_id"]) - self.assertEqual(ingredient_data["instance_id"], only_ingredient["instance_id"]) - self.assertEqual(ingredient_data["relationship"], only_ingredient["relationship"]) - - def test_read_file(self): - """Test reading a C2PA ingredient from a file.""" - temp_data_dir = os.path.join(self.data_dir, "temp_data") - os.makedirs(temp_data_dir, exist_ok=True) - - # self.testPath has C2PA metadata to read - file_json_with_dir = read_file(self.testPath, temp_data_dir) - - # Parse the JSON and verify specific fields - file_data = json.loads(file_json_with_dir) - expected_manifest_id = "contentauth:urn:uuid:c85a2b90-f1a0-4aa4-b17f-f938b475804e" - - # Verify some fields - self.assertEqual(file_data["active_manifest"], expected_manifest_id) - self.assertIn("manifests", file_data) - self.assertIn(expected_manifest_id, file_data["manifests"]) - - def test_sign_file(self): - """Test signing a file with C2PA manifest.""" - # Set up test paths - temp_data_dir = os.path.join(self.data_dir, "temp_data") - os.makedirs(temp_data_dir, exist_ok=True) - output_path = os.path.join(temp_data_dir, "signed_output.jpg") - - # Load test certificates and key - with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file: - certs = cert_file.read() - with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file: - key = key_file.read() - - # Create signer info - signer_info = C2paSignerInfo( - alg=b"es256", - sign_cert=certs, - private_key=key, - ta_url=b"http://timestamp.digicert.com" - ) - - # Create a simple manifest - manifest = { - "claim_generator": "python_internals_test", - "claim_generator_info": [{ - "name": "python_internals_test", - "version": "0.0.1", - }], - # Claim version has become mandatory for signing v1 claims - "claim_version": 1, - "format": "image/jpeg", - "title": "Python Test Signed Image", - "ingredients": [], - "assertions": [ - { - "label": "c2pa.actions", - "data": { - "actions": [ - { - "action": "c2pa.opened" - } - ] - } - } - ] - } - - # Convert manifest to JSON string - manifest_json = json.dumps(manifest) - - try: - sign_file( - self.testPath, - output_path, - manifest_json, - signer_info - ) - - finally: - # Clean up - if os.path.exists(output_path): - os.remove(output_path) - - def test_sign_file_does_not_exist_errors(self): - """Test signing a file with C2PA manifest.""" - # Set up test paths - temp_data_dir = os.path.join(self.data_dir, "temp_data") - os.makedirs(temp_data_dir, exist_ok=True) - output_path = os.path.join(temp_data_dir, "signed_output.jpg") - - # Load test certificates and key - with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file: - certs = cert_file.read() - with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file: - key = key_file.read() - - # Create signer info - signer_info = C2paSignerInfo( - alg=b"es256", - sign_cert=certs, - private_key=key, - ta_url=b"http://timestamp.digicert.com" - ) - - # Create a simple manifest - manifest = { - "claim_generator": "python_internals_test", - "claim_generator_info": [{ - "name": "python_internals_test", - "version": "0.0.1", - }], - # Claim version has become mandatory for signing v1 claims - "claim_version": 1, - "format": "image/jpeg", - "title": "Python Test Signed Image", - "ingredients": [], - "assertions": [ - { - "label": "c2pa.actions", - "data": { - "actions": [ - { - "action": "c2pa.opened" - } - ] - } - } - ] - } - - # Convert manifest to JSON string - manifest_json = json.dumps(manifest) - - try: - with self.assertRaises(Error): - sign_file( - "this-file-does-not-exist", - output_path, - manifest_json, - signer_info - ) - - finally: - # Clean up - if os.path.exists(output_path): - os.remove(output_path) - - def test_builder_sign_with_ingredient_from_file(self): - """Test Builder class operations with an ingredient added from file path.""" - - builder = Builder.from_json(self.manifestDefinition) - - # Test adding ingredient from file path - ingredient_json = '{"title": "Test Ingredient From File"}' - # Suppress the specific deprecation warning for this test, as this is a legacy method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath3) - - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - reader = Reader("image/jpeg", output) - json_data = reader.json() - manifest_data = json.loads(json_data) - - # Verify active manifest exists - self.assertIn("active_manifest", manifest_data) - active_manifest_id = manifest_data["active_manifest"] - - # Verify active manifest object exists - self.assertIn("manifests", manifest_data) - self.assertIn(active_manifest_id, manifest_data["manifests"]) - active_manifest = manifest_data["manifests"][active_manifest_id] - - # Verify ingredients array exists in active manifest - self.assertIn("ingredients", active_manifest) - self.assertIsInstance(active_manifest["ingredients"], list) - self.assertTrue(len(active_manifest["ingredients"]) > 0) - - # Verify the first ingredient's title matches what we set - first_ingredient = active_manifest["ingredients"][0] - self.assertEqual(first_ingredient["title"], "Test Ingredient From File") - - builder.close() - - def test_builder_sign_with_ingredient_dict_from_file(self): - """Test Builder class operations with an ingredient added from file path using a dictionary.""" - - builder = Builder.from_json(self.manifestDefinition) - - # Test adding ingredient from file path with a dictionary - ingredient_dict = {"title": "Test Ingredient From File"} - # Suppress the specific deprecation warning for this test, as this is a legacy method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - builder.add_ingredient_from_file_path(ingredient_dict, "image/jpeg", self.testPath3) - - with open(self.testPath2, "rb") as file: - output = io.BytesIO(bytearray()) - builder.sign(self.signer, "image/jpeg", file, output) - output.seek(0) - reader = Reader("image/jpeg", output) - json_data = reader.json() - manifest_data = json.loads(json_data) - - # Verify active manifest exists - self.assertIn("active_manifest", manifest_data) - active_manifest_id = manifest_data["active_manifest"] - - # Verify active manifest object exists - self.assertIn("manifests", manifest_data) - self.assertIn(active_manifest_id, manifest_data["manifests"]) - active_manifest = manifest_data["manifests"][active_manifest_id] - - # Verify ingredients array exists in active manifest - self.assertIn("ingredients", active_manifest) - self.assertIsInstance(active_manifest["ingredients"], list) - self.assertTrue(len(active_manifest["ingredients"]) > 0) - - # Verify the first ingredient's title matches what we set - first_ingredient = active_manifest["ingredients"][0] - self.assertEqual(first_ingredient["title"], "Test Ingredient From File") - - builder.close() - - def test_builder_add_ingredient_from_file_path(self): - """Test Builder class add_ingredient_from_file_path method.""" - - # Suppress the specific deprecation warning for this test, as this is a legacy method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - - builder = Builder.from_json(self.manifestDefinition) - - # Test adding ingredient from file path - ingredient_json = '{"test": "ingredient_from_file_path"}' - builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath) - - builder.close() - - def test_builder_add_ingredient_from_file_path_not_found(self): - """Test Builder class add_ingredient_from_file_path method.""" - - # Suppress the specific deprecation warning for this test, as this is a legacy method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - - builder = Builder.from_json(self.manifestDefinition) - - # Test adding ingredient from file path - ingredient_json = '{"test": "ingredient_from_file_path"}' - - with self.assertRaises(Error.FileNotFound): - builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", "this-file-path-does-not-exist") - - def test_sign_file_using_callback_signer_overloads(self): - """Test signing a file using the sign_file function with a Signer object.""" - # 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_callback.jpg") - - # Create signer with callback - signer = Signer.from_callback( - callback=self.callback_signer_es256, - alg=SigningAlg.ES256, - certs=self.certs.decode('utf-8'), - tsa_url="http://timestamp.digicert.com" - ) - - # Overload that returns a JSON string - result_json = sign_file( - self.testPath, - output_path, - self.manifestDefinition, - signer, - False - ) - - # Verify the output file was created - self.assertTrue(os.path.exists(output_path)) - - # Verify the result is JSON - self.assertIsInstance(result_json, str) - self.assertGreater(len(result_json), 0) - - manifest_data = json.loads(result_json) - self.assertIn("manifests", manifest_data) - self.assertIn("active_manifest", manifest_data) - - output_path_bytes = os.path.join(temp_dir, "signed_output_callback_bytes.jpg") - # Overload that returns bytes - result_bytes = sign_file( - self.testPath, - output_path_bytes, - self.manifestDefinition, - signer, - True - ) - - # Verify the output file was created - self.assertTrue(os.path.exists(output_path_bytes)) - - # Verify the result is bytes - self.assertIsInstance(result_bytes, bytes) - self.assertGreater(len(result_bytes), 0) - - # Read the signed file and verify the manifest contains expected content - with open(output_path, "rb") as file: - reader = Reader("image/jpeg", file) - file_manifest_json = reader.json() - self.assertIn("Python Test", file_manifest_json) - - finally: - shutil.rmtree(temp_dir) - - def test_sign_file_overloads(self): - """Test that the overloaded sign_file function works with both parameter types.""" - # Create a temporary directory for the test - temp_dir = tempfile.mkdtemp() - try: - # Test with C2paSignerInfo - output_path_1 = os.path.join(temp_dir, "signed_output_1.jpg") - - # Load test certificates and key - with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file: - certs = cert_file.read() - with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file: - key = key_file.read() - - # Create signer info - signer_info = C2paSignerInfo( - alg=b"es256", - sign_cert=certs, - private_key=key, - ta_url=b"http://timestamp.digicert.com" - ) - - # Test with C2paSignerInfo parameter - JSON return - result_1 = sign_file( - self.testPath, - output_path_1, - self.manifestDefinition, - signer_info, - False - ) - - self.assertIsInstance(result_1, str) - self.assertTrue(os.path.exists(output_path_1)) - - # Test with C2paSignerInfo parameter - bytes return - output_path_1_bytes = os.path.join(temp_dir, "signed_output_1_bytes.jpg") - result_1_bytes = sign_file( - self.testPath, - output_path_1_bytes, - self.manifestDefinition, - signer_info, - True - ) - - self.assertIsInstance(result_1_bytes, bytes) - self.assertTrue(os.path.exists(output_path_1_bytes)) - - # Test with Signer object - output_path_2 = os.path.join(temp_dir, "signed_output_2.jpg") - - # Create a signer from the signer info - signer = Signer.from_info(signer_info) - - # Test with Signer parameter - JSON return - result_2 = sign_file( - self.testPath, - output_path_2, - self.manifestDefinition, - signer, - False - ) - - self.assertIsInstance(result_2, str) - self.assertTrue(os.path.exists(output_path_2)) - - # Test with Signer parameter - bytes return - output_path_2_bytes = os.path.join(temp_dir, "signed_output_2_bytes.jpg") - result_2_bytes = sign_file( - self.testPath, - output_path_2_bytes, - self.manifestDefinition, - signer, - True - ) - - self.assertIsInstance(result_2_bytes, bytes) - self.assertTrue(os.path.exists(output_path_2_bytes)) - - # Both JSON results should be similar (same manifest structure) - manifest_1 = json.loads(result_1) - manifest_2 = json.loads(result_2) - - self.assertIn("manifests", manifest_1) - self.assertIn("manifests", manifest_2) - self.assertIn("active_manifest", manifest_1) - self.assertIn("active_manifest", manifest_2) - - # Both bytes results should be non-empty - self.assertGreater(len(result_1_bytes), 0) - self.assertGreater(len(result_2_bytes), 0) - - finally: - # Clean up the temporary directory - shutil.rmtree(temp_dir) - def test_sign_file_callback_signer_reports_error(self): """Test signing a file using the sign_file method with a callback that reports an error."""