c2pa-python 0.35.0__tar.gz → 0.35.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {c2pa_python-0.35.0/src/c2pa_python.egg-info → c2pa_python-0.35.1}/PKG-INFO +2 -2
  2. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/pyproject.toml +3 -2
  3. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa/c2pa.py +86 -8
  4. {c2pa_python-0.35.0 → c2pa_python-0.35.1/src/c2pa_python.egg-info}/PKG-INFO +2 -2
  5. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/tests/test_unit_tests.py +557 -1
  6. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/LICENSE-APACHE +0 -0
  7. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/LICENSE-MIT +0 -0
  8. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/MANIFEST.in +0 -0
  9. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/README.md +0 -0
  10. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/requirements.txt +0 -0
  11. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/scripts/download_artifacts.py +0 -0
  12. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/setup.cfg +0 -0
  13. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/setup.py +0 -0
  14. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa/__init__.py +0 -0
  15. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa/build.py +0 -0
  16. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa/lib.py +0 -0
  17. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.35.0 → c2pa_python-0.35.1}/tests/test_unit_tests_threaded.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.35.0
3
+ Version: 0.35.1
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
- Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
6
+ Maintainer-email: Gavin Peacock <gpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
7
7
  License: MIT OR Apache-2.0
8
8
  Project-URL: homepage, https://contentauthenticity.org
9
9
  Project-URL: repository, https://github.com/contentauth/c2pa-python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "c2pa-python"
7
- version = "0.35.0"
7
+ version = "0.35.1"
8
8
  requires-python = ">=3.10"
9
9
  description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -17,7 +17,8 @@ classifiers = [
17
17
  "Operating System :: Microsoft :: Windows"
18
18
  ]
19
19
  maintainers = [
20
- {name = "Gavin Peacock", email = "gpeacock@adobe.com"}
20
+ {name = "Gavin Peacock", email = "gpeacock@adobe.com"},
21
+ {name = "Tania Mathern", email = "mathern@adobe.com"}
21
22
  ]
22
23
  urls = {homepage = "https://contentauthenticity.org", repository = "https://github.com/contentauth/c2pa-python"}
23
24
  dependencies = [
@@ -18,6 +18,7 @@ import logging
18
18
  import sys
19
19
  import os
20
20
  import warnings
21
+ import weakref
21
22
  from abc import ABC, abstractmethod
22
23
  from pathlib import Path
23
24
  from typing import Optional, Union, Callable, Any, overload
@@ -65,6 +66,8 @@ _REQUIRED_FUNCTIONS = [
65
66
  'c2pa_builder_add_ingredient_from_stream',
66
67
  'c2pa_builder_add_action',
67
68
  'c2pa_builder_to_archive',
69
+ 'c2pa_builder_write_ingredient_archive',
70
+ 'c2pa_builder_add_ingredient_from_archive',
68
71
  'c2pa_builder_sign',
69
72
  'c2pa_builder_sign_context',
70
73
  'c2pa_builder_from_context',
@@ -623,6 +626,15 @@ _setup_function(_lib.c2pa_builder_add_action,
623
626
  _setup_function(_lib.c2pa_builder_to_archive,
624
627
  [ctypes.POINTER(C2paBuilder), ctypes.POINTER(C2paStream)],
625
628
  ctypes.c_int)
629
+ _setup_function(_lib.c2pa_builder_write_ingredient_archive,
630
+ [ctypes.POINTER(C2paBuilder),
631
+ ctypes.c_char_p,
632
+ ctypes.POINTER(C2paStream)],
633
+ ctypes.c_int)
634
+ _setup_function(_lib.c2pa_builder_add_ingredient_from_archive,
635
+ [ctypes.POINTER(C2paBuilder),
636
+ ctypes.POINTER(C2paStream)],
637
+ ctypes.c_int)
626
638
  _setup_function(_lib.c2pa_builder_sign,
627
639
  [ctypes.POINTER(C2paBuilder),
628
640
  ctypes.c_char_p,
@@ -1614,6 +1626,10 @@ class Stream:
1614
1626
 
1615
1627
  self._file_like_stream = file_like_stream
1616
1628
 
1629
+ # Weakref breaks avoids references cycles:
1630
+ # Stream -> ctypes_cb -> closure -> Stream.
1631
+ _weak_self = weakref.ref(self)
1632
+
1617
1633
  def read_callback(ctx, data, length):
1618
1634
  """Callback function for reading data from the Python stream.
1619
1635
 
@@ -1632,13 +1648,16 @@ class Stream:
1632
1648
  Returns:
1633
1649
  Number of bytes read, or -1 on error
1634
1650
  """
1635
- if not self._initialized or self._closed:
1651
+ s = _weak_self()
1652
+ if s is None:
1653
+ return -1
1654
+ if not s._initialized or s._closed:
1636
1655
  return -1
1637
1656
  try:
1638
1657
  if not data or length <= 0:
1639
1658
  return -1
1640
1659
 
1641
- stream = self._file_like_stream
1660
+ stream = s._file_like_stream
1642
1661
  readinto = getattr(stream, "readinto", None)
1643
1662
  if readinto is not None:
1644
1663
  # Most streams have readinto
@@ -1689,8 +1708,11 @@ class Stream:
1689
1708
  Returns:
1690
1709
  New position in the stream, or -1 on error
1691
1710
  """
1692
- file_stream = self._file_like_stream
1693
- if not self._initialized or self._closed:
1711
+ s = _weak_self()
1712
+ if s is None:
1713
+ return -1
1714
+ file_stream = s._file_like_stream
1715
+ if not s._initialized or s._closed:
1694
1716
  return -1
1695
1717
  try:
1696
1718
  # Fall back to tell() only for stream objects that do not
@@ -1718,13 +1740,16 @@ class Stream:
1718
1740
  Returns:
1719
1741
  Number of bytes written, or -1 on error
1720
1742
  """
1721
- if not self._initialized or self._closed:
1743
+ s = _weak_self()
1744
+ if s is None:
1745
+ return -1
1746
+ if not s._initialized or s._closed:
1722
1747
  return -1
1723
1748
  try:
1724
1749
  if not data or length <= 0:
1725
1750
  return -1
1726
1751
 
1727
- self._file_like_stream.write(ctypes.string_at(data, length))
1752
+ s._file_like_stream.write(ctypes.string_at(data, length))
1728
1753
  return length
1729
1754
  except Exception:
1730
1755
  return -1
@@ -1743,10 +1768,13 @@ class Stream:
1743
1768
  Returns:
1744
1769
  0 on success, -1 on error
1745
1770
  """
1746
- if not self._initialized or self._closed:
1771
+ s = _weak_self()
1772
+ if s is None:
1773
+ return -1
1774
+ if not s._initialized or s._closed:
1747
1775
  return -1
1748
1776
  try:
1749
- self._file_like_stream.flush()
1777
+ s._file_like_stream.flush()
1750
1778
  return 0
1751
1779
  except Exception:
1752
1780
  return -1
@@ -2898,6 +2926,7 @@ class Builder(ManagedResource):
2898
2926
  'url_error': "Error setting remote URL: {}",
2899
2927
  'resource_error': "Error adding resource: {}",
2900
2928
  'ingredient_error': "Error adding ingredient: {}",
2929
+ 'archive_read_error': "Error loading ingredient from archive: {}",
2901
2930
  'action_error': "Error adding action: {}",
2902
2931
  'archive_error': "Error writing archive: {}",
2903
2932
  'sign_error': "Error during signing: {}",
@@ -3287,6 +3316,55 @@ class Builder(ManagedResource):
3287
3316
  Builder._ERROR_MESSAGES["archive_error"].format("Unknown error"),
3288
3317
  check=lambda r: r != 0)
3289
3318
 
3319
+ def write_ingredient_archive(self, ingredient_id: str, stream: Any) -> None:
3320
+ """Write a single-ingredient archive for the named ingredient.
3321
+ The archive is in C2PA format.
3322
+
3323
+ Args:
3324
+ ingredient_id: Identifier of the ingredient within this builder;
3325
+ matched against the ingredient's label or instance_id
3326
+ stream: Writable, seekable stream to receive the archive
3327
+
3328
+ Raises:
3329
+ C2paError: If there was an error writing the archive
3330
+ """
3331
+ self._ensure_valid_state()
3332
+
3333
+ ingredient_id_str = _to_utf8_bytes(ingredient_id, "ingredient_id")
3334
+
3335
+ with Stream(stream) as stream_obj:
3336
+ result = _lib.c2pa_builder_write_ingredient_archive(
3337
+ self._handle, ingredient_id_str, stream_obj._stream)
3338
+
3339
+ _check_ffi_operation_result(result,
3340
+ Builder._ERROR_MESSAGES["archive_error"].format(
3341
+ "Unknown error"
3342
+ ),
3343
+ check=lambda r: r != 0)
3344
+
3345
+ def add_ingredient_from_archive(self, stream: Any) -> None:
3346
+ """Add an ingredient from a per-ingredient archive stream.
3347
+ The archive must be in C2PA format, as written by
3348
+ write_ingredient_archive.
3349
+
3350
+ Args:
3351
+ stream: Readable, seekable stream containing the ingredient archive
3352
+
3353
+ Raises:
3354
+ C2paError: If there was an error reading the archive
3355
+ """
3356
+ self._ensure_valid_state()
3357
+
3358
+ with Stream(stream) as stream_obj:
3359
+ result = _lib.c2pa_builder_add_ingredient_from_archive(
3360
+ self._handle, stream_obj._stream)
3361
+
3362
+ _check_ffi_operation_result(result,
3363
+ Builder._ERROR_MESSAGES["archive_read_error"].format(
3364
+ "Unknown error"
3365
+ ),
3366
+ check=lambda r: r != 0)
3367
+
3290
3368
  def with_archive(self, stream: Any) -> 'Builder':
3291
3369
  """Load an archive into this Builder instance, replacing its
3292
3370
  manifest definition. The archive carries only the
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.35.0
3
+ Version: 0.35.1
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
- Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
6
+ Maintainer-email: Gavin Peacock <gpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
7
7
  License: MIT OR Apache-2.0
8
8
  Project-URL: homepage, https://contentauthenticity.org
9
9
  Project-URL: repository, https://github.com/contentauth/c2pa-python
@@ -23,7 +23,6 @@ from cryptography.hazmat.backends import default_backend
23
23
  import tempfile
24
24
  import shutil
25
25
  import ctypes
26
- import toml
27
26
  import threading
28
27
 
29
28
  # Suppress deprecation warnings
@@ -1945,6 +1944,500 @@ class TestBuilderWithSigner(unittest.TestCase):
1945
1944
  self.assertIsNotNone(result.get('validation_state'))
1946
1945
  self.assertEqual(result.get('validation_state'), "Trusted")
1947
1946
 
1947
+ def test_write_ingredient_archive_produces_readable_archive(self):
1948
+ manifest = {
1949
+ "claim_generator_info": [{"name": "c2pa-test", "version": "0.1.0"}],
1950
+ "assertions": [],
1951
+ }
1952
+ builder = Builder.from_json(manifest)
1953
+ ingredient_json = {
1954
+ "title": "A.jpg",
1955
+ "relationship": "componentOf",
1956
+ "instance_id": "ingredient-A",
1957
+ }
1958
+ with open(self.testPath, "rb") as f:
1959
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
1960
+ archive = io.BytesIO()
1961
+ builder.write_ingredient_archive("ingredient-A", archive)
1962
+ builder.close()
1963
+ self.assertGreater(len(archive.getvalue()), 0)
1964
+ archive.close()
1965
+
1966
+ def test_add_ingredient_from_archive_roundtrip(self):
1967
+ manifest = {
1968
+ "claim_generator_info": [{"name": "c2pa-test", "version": "0.1.0"}],
1969
+ "assertions": [],
1970
+ }
1971
+ builder = Builder.from_json(manifest)
1972
+ ingredient_json = {
1973
+ "title": "A.jpg",
1974
+ "relationship": "componentOf",
1975
+ "instance_id": "catalog:ingredient-A",
1976
+ }
1977
+ with open(self.testPath, "rb") as f:
1978
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
1979
+ archive = io.BytesIO()
1980
+ builder.write_ingredient_archive("catalog:ingredient-A", archive)
1981
+ builder.close()
1982
+
1983
+ archive.seek(0)
1984
+ builder2 = Builder.from_json(manifest)
1985
+ builder2.add_ingredient_from_archive(archive)
1986
+ archive.close()
1987
+
1988
+ output = io.BytesIO()
1989
+ with open(self.testPath, "rb") as f:
1990
+ builder2.sign(self.signer, "image/jpeg", f, output)
1991
+ builder2.close()
1992
+
1993
+ output.seek(0)
1994
+ reader = Reader("image/jpeg", output)
1995
+ json_data = reader.json()
1996
+ self.assertIn("A.jpg", json_data)
1997
+ output.close()
1998
+
1999
+ def test_add_ingredient_from_archive_preserves_instance_id(self):
2000
+ manifest = {
2001
+ "claim_generator_info": [{"name": "c2pa-test", "version": "0.1.0"}],
2002
+ "assertions": [],
2003
+ }
2004
+ archive_builder = Builder.from_json(manifest)
2005
+ ingredient_json = {
2006
+ "title": "photo.jpg",
2007
+ "relationship": "parentOf",
2008
+ "instance_id": "my-ingredient",
2009
+ }
2010
+ with open(self.testPath, "rb") as f:
2011
+ archive_builder.add_ingredient(ingredient_json, "image/jpeg", f)
2012
+ archive = io.BytesIO()
2013
+ archive_builder.write_ingredient_archive("my-ingredient", archive)
2014
+ archive_builder.close()
2015
+
2016
+ archive.seek(0)
2017
+ signing_builder = Builder.from_json(manifest)
2018
+ signing_builder.add_ingredient_from_archive(archive)
2019
+ archive.close()
2020
+
2021
+ output = io.BytesIO()
2022
+ with open(self.testPath, "rb") as f:
2023
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2024
+ signing_builder.close()
2025
+
2026
+ output.seek(0)
2027
+ reader = Reader("image/jpeg", output)
2028
+ json_data = reader.json()
2029
+ self.assertIn("photo.jpg", json_data)
2030
+ output.close()
2031
+
2032
+ def test_add_ingredient_from_archive_preserves_instance_id_component_of(self):
2033
+ manifest = {
2034
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2035
+ "assertions": [],
2036
+ }
2037
+ archive_builder = Builder.from_json(manifest)
2038
+ ingredient_json = {
2039
+ "title": "photo.jpg",
2040
+ "relationship": "componentOf",
2041
+ "instance_id": "my-ingredient",
2042
+ }
2043
+ with open(self.testPath, "rb") as f:
2044
+ archive_builder.add_ingredient(ingredient_json, "image/jpeg", f)
2045
+ archive = io.BytesIO()
2046
+ archive_builder.write_ingredient_archive("my-ingredient", archive)
2047
+ archive_builder.close()
2048
+
2049
+ archive.seek(0)
2050
+ signing_builder = Builder.from_json(manifest)
2051
+ signing_builder.add_ingredient_from_archive(archive)
2052
+ archive.close()
2053
+
2054
+ output = io.BytesIO()
2055
+ with open(self.testPath, "rb") as f:
2056
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2057
+ signing_builder.close()
2058
+
2059
+ output.seek(0)
2060
+ reader = Reader("image/jpeg", output)
2061
+ json_data = reader.json()
2062
+ self.assertIn("photo.jpg", json_data)
2063
+ self.assertIn("componentOf", json_data)
2064
+ output.close()
2065
+
2066
+ def test_add_ingredient_from_archive_preserves_instance_id_input_to(self):
2067
+ manifest = {
2068
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2069
+ "assertions": [],
2070
+ }
2071
+ archive_builder = Builder.from_json(manifest)
2072
+ ingredient_json = {
2073
+ "title": "photo.jpg",
2074
+ "relationship": "inputTo",
2075
+ "instance_id": "my-ingredient",
2076
+ }
2077
+ with open(self.testPath, "rb") as f:
2078
+ archive_builder.add_ingredient(ingredient_json, "image/jpeg", f)
2079
+ archive = io.BytesIO()
2080
+ archive_builder.write_ingredient_archive("my-ingredient", archive)
2081
+ archive_builder.close()
2082
+
2083
+ archive.seek(0)
2084
+ signing_builder = Builder.from_json(manifest)
2085
+ signing_builder.add_ingredient_from_archive(archive)
2086
+ archive.close()
2087
+
2088
+ output = io.BytesIO()
2089
+ with open(self.testPath, "rb") as f:
2090
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2091
+ signing_builder.close()
2092
+
2093
+ output.seek(0)
2094
+ reader = Reader("image/jpeg", output)
2095
+ json_data = reader.json()
2096
+ self.assertIn("photo.jpg", json_data)
2097
+ self.assertIn("inputTo", json_data)
2098
+ output.close()
2099
+
2100
+ def test_add_ingredient_from_archive_roundtrip_parent_of(self):
2101
+ manifest = {
2102
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2103
+ "assertions": [],
2104
+ }
2105
+ builder = Builder.from_json(manifest)
2106
+ ingredient_json = {
2107
+ "title": "A.jpg",
2108
+ "relationship": "parentOf",
2109
+ "instance_id": "catalog:ingredient-A",
2110
+ }
2111
+ with open(self.testPath, "rb") as f:
2112
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
2113
+ archive = io.BytesIO()
2114
+ builder.write_ingredient_archive("catalog:ingredient-A", archive)
2115
+ builder.close()
2116
+
2117
+ archive.seek(0)
2118
+ builder2 = Builder.from_json(manifest)
2119
+ builder2.add_ingredient_from_archive(archive)
2120
+ archive.close()
2121
+
2122
+ output = io.BytesIO()
2123
+ with open(self.testPath, "rb") as f:
2124
+ builder2.sign(self.signer, "image/jpeg", f, output)
2125
+ builder2.close()
2126
+
2127
+ output.seek(0)
2128
+ reader = Reader("image/jpeg", output)
2129
+ json_data = reader.json()
2130
+ self.assertIn("A.jpg", json_data)
2131
+ self.assertIn("parentOf", json_data)
2132
+ output.close()
2133
+
2134
+ def test_add_ingredient_from_archive_roundtrip_input_to(self):
2135
+ manifest = {
2136
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2137
+ "assertions": [],
2138
+ }
2139
+ builder = Builder.from_json(manifest)
2140
+ ingredient_json = {
2141
+ "title": "A.jpg",
2142
+ "relationship": "inputTo",
2143
+ "instance_id": "catalog:ingredient-A",
2144
+ }
2145
+ with open(self.testPath, "rb") as f:
2146
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
2147
+ archive = io.BytesIO()
2148
+ builder.write_ingredient_archive("catalog:ingredient-A", archive)
2149
+ builder.close()
2150
+
2151
+ archive.seek(0)
2152
+ builder2 = Builder.from_json(manifest)
2153
+ builder2.add_ingredient_from_archive(archive)
2154
+ archive.close()
2155
+
2156
+ output = io.BytesIO()
2157
+ with open(self.testPath, "rb") as f:
2158
+ builder2.sign(self.signer, "image/jpeg", f, output)
2159
+ builder2.close()
2160
+
2161
+ output.seek(0)
2162
+ reader = Reader("image/jpeg", output)
2163
+ json_data = reader.json()
2164
+ self.assertIn("A.jpg", json_data)
2165
+ self.assertIn("inputTo", json_data)
2166
+ output.close()
2167
+
2168
+ def test_ingredient_from_archive_linked_to_opened_action(self):
2169
+ ingredient_id = "xmp:iid:archive-parent-001"
2170
+ manifest = {
2171
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2172
+ "assertions": [
2173
+ {
2174
+ "label": "c2pa.actions.v2",
2175
+ "data": {
2176
+ "actions": [
2177
+ {
2178
+ "action": "c2pa.opened",
2179
+ "parameters": {"ingredientIds": [ingredient_id]},
2180
+ }
2181
+ ]
2182
+ },
2183
+ }
2184
+ ],
2185
+ }
2186
+ archive_builder = Builder.from_json(manifest)
2187
+ with open(self.testPath, "rb") as f:
2188
+ archive_builder.add_ingredient(
2189
+ {"title": "photo.jpg", "relationship": "parentOf", "instance_id": ingredient_id},
2190
+ "image/jpeg", f)
2191
+ archive = io.BytesIO()
2192
+ archive_builder.write_ingredient_archive(ingredient_id, archive)
2193
+ archive_builder.close()
2194
+
2195
+ archive.seek(0)
2196
+ signing_builder = Builder.from_json(manifest)
2197
+ signing_builder.add_ingredient_from_archive(archive)
2198
+ archive.close()
2199
+
2200
+ output = io.BytesIO()
2201
+ with open(self.testPath, "rb") as f:
2202
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2203
+ signing_builder.close()
2204
+
2205
+ output.seek(0)
2206
+ reader = Reader("image/jpeg", output)
2207
+ json_data = reader.json()
2208
+ self.assertIn("c2pa.opened", json_data)
2209
+ self.assertIn(ingredient_id, json_data)
2210
+ output.close()
2211
+
2212
+ def test_ingredient_from_archive_linked_to_placed_action(self):
2213
+ ingredient_id = "xmp:iid:archive-component-001"
2214
+ manifest = {
2215
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2216
+ "assertions": [
2217
+ {
2218
+ "label": "c2pa.actions.v2",
2219
+ "data": {
2220
+ "actions": [
2221
+ {
2222
+ "action": "c2pa.placed",
2223
+ "parameters": {"ingredientIds": [ingredient_id]},
2224
+ }
2225
+ ]
2226
+ },
2227
+ }
2228
+ ],
2229
+ }
2230
+ archive_builder = Builder.from_json(manifest)
2231
+ with open(self.testPath, "rb") as f:
2232
+ archive_builder.add_ingredient(
2233
+ {"title": "photo.jpg", "relationship": "componentOf", "instance_id": ingredient_id},
2234
+ "image/jpeg", f)
2235
+ archive = io.BytesIO()
2236
+ archive_builder.write_ingredient_archive(ingredient_id, archive)
2237
+ archive_builder.close()
2238
+
2239
+ archive.seek(0)
2240
+ signing_builder = Builder.from_json(manifest)
2241
+ signing_builder.add_ingredient_from_archive(archive)
2242
+ archive.close()
2243
+
2244
+ output = io.BytesIO()
2245
+ with open(self.testPath, "rb") as f:
2246
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2247
+ signing_builder.close()
2248
+
2249
+ output.seek(0)
2250
+ reader = Reader("image/jpeg", output)
2251
+ json_data = reader.json()
2252
+ self.assertIn("c2pa.placed", json_data)
2253
+ self.assertIn(ingredient_id, json_data)
2254
+ output.close()
2255
+
2256
+ def test_ingredient_from_archive_linked_to_edited_action(self):
2257
+ ingredient_id = "xmp:iid:archive-input-001"
2258
+ manifest = {
2259
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2260
+ "assertions": [
2261
+ {
2262
+ "label": "c2pa.actions.v2",
2263
+ "data": {
2264
+ "actions": [
2265
+ {
2266
+ "action": "c2pa.edited",
2267
+ "parameters": {"ingredientIds": [ingredient_id]},
2268
+ }
2269
+ ]
2270
+ },
2271
+ }
2272
+ ],
2273
+ }
2274
+ archive_builder = Builder.from_json(manifest)
2275
+ with open(self.testPath, "rb") as f:
2276
+ archive_builder.add_ingredient(
2277
+ {"title": "photo.jpg", "relationship": "inputTo", "instance_id": ingredient_id},
2278
+ "image/jpeg", f)
2279
+ archive = io.BytesIO()
2280
+ archive_builder.write_ingredient_archive(ingredient_id, archive)
2281
+ archive_builder.close()
2282
+
2283
+ archive.seek(0)
2284
+ signing_builder = Builder.from_json(manifest)
2285
+ signing_builder.add_ingredient_from_archive(archive)
2286
+ archive.close()
2287
+
2288
+ output = io.BytesIO()
2289
+ with open(self.testPath, "rb") as f:
2290
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2291
+ signing_builder.close()
2292
+
2293
+ output.seek(0)
2294
+ reader = Reader("image/jpeg", output)
2295
+ json_data = reader.json()
2296
+ self.assertIn("c2pa.edited", json_data)
2297
+ self.assertIn(ingredient_id, json_data)
2298
+ output.close()
2299
+
2300
+ def test_add_two_ingredient_archives_to_one_builder(self):
2301
+ manifest = {
2302
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2303
+ "assertions": [],
2304
+ }
2305
+ archives = []
2306
+ for title, instance_id in [("A.jpg", "ingredient-A"), ("B.jpg", "ingredient-B")]:
2307
+ archive_builder = Builder.from_json(manifest)
2308
+ ingredient_json = {
2309
+ "title": title,
2310
+ "relationship": "componentOf",
2311
+ "instance_id": instance_id,
2312
+ }
2313
+ with open(self.testPath, "rb") as f:
2314
+ archive_builder.add_ingredient(ingredient_json, "image/jpeg", f)
2315
+ archive = io.BytesIO()
2316
+ archive_builder.write_ingredient_archive(instance_id, archive)
2317
+ archive_builder.close()
2318
+ archive.seek(0)
2319
+ archives.append(archive)
2320
+
2321
+ signing_builder = Builder.from_json(manifest)
2322
+ for archive in archives:
2323
+ signing_builder.add_ingredient_from_archive(archive)
2324
+ archive.close()
2325
+
2326
+ output = io.BytesIO()
2327
+ with open(self.testPath, "rb") as f:
2328
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2329
+ signing_builder.close()
2330
+
2331
+ output.seek(0)
2332
+ reader = Reader("image/jpeg", output)
2333
+ json_data = reader.json()
2334
+ self.assertIn("A.jpg", json_data)
2335
+ self.assertIn("B.jpg", json_data)
2336
+ output.close()
2337
+
2338
+ def test_write_ingredient_archive_only_contains_requested_ingredient(self):
2339
+ manifest = {
2340
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2341
+ "assertions": [],
2342
+ }
2343
+ archive_builder = Builder.from_json(manifest)
2344
+ for title, instance_id in [("A.jpg", "ingredient-A"), ("B.jpg", "ingredient-B")]:
2345
+ ingredient_json = {
2346
+ "title": title,
2347
+ "relationship": "componentOf",
2348
+ "instance_id": instance_id,
2349
+ }
2350
+ with open(self.testPath, "rb") as f:
2351
+ archive_builder.add_ingredient(ingredient_json, "image/jpeg", f)
2352
+ archive = io.BytesIO()
2353
+ archive_builder.write_ingredient_archive("ingredient-A", archive)
2354
+ archive_builder.close()
2355
+
2356
+ archive.seek(0)
2357
+ signing_builder = Builder.from_json(manifest)
2358
+ signing_builder.add_ingredient_from_archive(archive)
2359
+ archive.close()
2360
+
2361
+ output = io.BytesIO()
2362
+ with open(self.testPath, "rb") as f:
2363
+ signing_builder.sign(self.signer, "image/jpeg", f, output)
2364
+ signing_builder.close()
2365
+
2366
+ output.seek(0)
2367
+ reader = Reader("image/jpeg", output)
2368
+ json_data = reader.json()
2369
+ self.assertIn("A.jpg", json_data)
2370
+ self.assertNotIn("B.jpg", json_data)
2371
+ output.close()
2372
+
2373
+ def test_write_ingredient_archive_matches_label_or_instance_id(self):
2374
+ manifest = {
2375
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2376
+ "assertions": [],
2377
+ }
2378
+ ingredient_json = {
2379
+ "title": "photo.jpg",
2380
+ "relationship": "componentOf",
2381
+ "label": "my-label",
2382
+ "instance_id": "my-instance-id",
2383
+ }
2384
+ builder = Builder.from_json(manifest)
2385
+ with open(self.testPath, "rb") as f:
2386
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
2387
+
2388
+ by_label = io.BytesIO()
2389
+ builder.write_ingredient_archive("my-label", by_label)
2390
+ self.assertGreater(len(by_label.getvalue()), 0)
2391
+ by_label.close()
2392
+
2393
+ by_instance_id = io.BytesIO()
2394
+ builder.write_ingredient_archive("my-instance-id", by_instance_id)
2395
+ self.assertGreater(len(by_instance_id.getvalue()), 0)
2396
+ by_instance_id.close()
2397
+
2398
+ builder.close()
2399
+
2400
+ def test_write_ingredient_archive_unknown_id_raises(self):
2401
+ manifest = {
2402
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2403
+ "assertions": [],
2404
+ }
2405
+ builder = Builder.from_json(manifest)
2406
+ ingredient_json = {
2407
+ "title": "photo.jpg",
2408
+ "relationship": "componentOf",
2409
+ "instance_id": "ingredient-A",
2410
+ }
2411
+ with open(self.testPath, "rb") as f:
2412
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
2413
+ with self.assertRaises(Error):
2414
+ builder.write_ingredient_archive("nonexistent-id", io.BytesIO())
2415
+ builder.close()
2416
+
2417
+ def test_add_ingredient_from_archive_invalid_data_raises(self):
2418
+ manifest = {
2419
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2420
+ "assertions": [],
2421
+ }
2422
+ builder = Builder.from_json(manifest)
2423
+ with self.assertRaises(Error):
2424
+ builder.add_ingredient_from_archive(io.BytesIO(b"not a c2pa archive"))
2425
+ with self.assertRaises(Error):
2426
+ builder.add_ingredient_from_archive(io.BytesIO())
2427
+ builder.close()
2428
+
2429
+ def test_ingredient_archive_methods_raise_on_closed_builder(self):
2430
+ manifest = {
2431
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
2432
+ "assertions": [],
2433
+ }
2434
+ builder = Builder.from_json(manifest)
2435
+ builder.close()
2436
+ with self.assertRaises(Error):
2437
+ builder.write_ingredient_archive("ingredient-A", io.BytesIO())
2438
+ with self.assertRaises(Error):
2439
+ builder.add_ingredient_from_archive(io.BytesIO())
2440
+
1948
2441
  def test_remote_sign(self):
1949
2442
  with open(self.testPath, "rb") as file:
1950
2443
  builder = Builder(self.manifestDefinition)
@@ -6255,5 +6748,68 @@ class TestContextIntegration(TestContextAPIs):
6255
6748
  context.close()
6256
6749
 
6257
6750
 
6751
+ class TestStreamReferences(unittest.TestCase):
6752
+
6753
+ def test_stream_collected_after_del(self):
6754
+ """Stream must be collected by reference counting."""
6755
+ import gc
6756
+ import weakref
6757
+ gc.collect()
6758
+ garbage_before = len(gc.garbage)
6759
+
6760
+ buf = io.BytesIO(b"hello world")
6761
+ s = Stream(buf)
6762
+ ref = weakref.ref(s)
6763
+ del s
6764
+
6765
+ # Trigger gc, we want to verify it's collected
6766
+ # collected now (1 gc call) means no gc cycle breaker needed
6767
+ # aka ref cycle did not happen.
6768
+ gc.collect()
6769
+
6770
+ self.assertIsNone(ref(), "Stream not collected")
6771
+ self.assertEqual(len(gc.garbage), garbage_before,
6772
+ "Stream added objects to gc.garbage")
6773
+
6774
+ def test_stream_not_added_to_gc_garbage_list(self):
6775
+ """Creating and dropping many Streams must not grow gc.garbage."""
6776
+ import gc
6777
+ gc.collect()
6778
+ gc.garbage.clear()
6779
+
6780
+ for _ in range(20):
6781
+ s = Stream(io.BytesIO(b"data"))
6782
+ del s
6783
+
6784
+ gc.collect()
6785
+ self.assertEqual(len(gc.garbage), 0,
6786
+ f"gc.garbage unexpectedly non-empty: {gc.garbage}")
6787
+
6788
+ def test_callbacks_return_minus_one_after_stream_collected(self):
6789
+ """Callbacks must return -1 gracefully when the Stream has been GC'd."""
6790
+ import gc
6791
+ import weakref
6792
+
6793
+ buf = io.BytesIO(b"test")
6794
+ s = Stream(buf)
6795
+
6796
+ read_cb = s._read_cb
6797
+ seek_cb = s._seek_cb
6798
+ write_cb = s._write_cb
6799
+ flush_cb = s._flush_cb
6800
+ ref = weakref.ref(s)
6801
+ del s
6802
+ gc.collect()
6803
+
6804
+ # Stream should be gone.
6805
+ self.assertIsNone(ref(), "Stream not collected before callback test")
6806
+
6807
+ # All callbacks must return -1 without crashing.
6808
+ self.assertEqual(read_cb(None, None, 0), -1)
6809
+ self.assertEqual(seek_cb(None, 0, 0), -1)
6810
+ self.assertEqual(write_cb(None, None, 0), -1)
6811
+ self.assertEqual(flush_cb(None), -1)
6812
+
6813
+
6258
6814
  if __name__ == '__main__':
6259
6815
  unittest.main(warnings='ignore')
File without changes
File without changes
File without changes
File without changes
File without changes