c2pa-python 0.35.0__tar.gz → 0.35.2__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.
- {c2pa_python-0.35.0/src/c2pa_python.egg-info → c2pa_python-0.35.2}/PKG-INFO +2 -2
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/pyproject.toml +3 -2
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa/c2pa.py +92 -14
- {c2pa_python-0.35.0 → c2pa_python-0.35.2/src/c2pa_python.egg-info}/PKG-INFO +2 -2
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/tests/test_unit_tests.py +670 -1
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/LICENSE-APACHE +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/LICENSE-MIT +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/MANIFEST.in +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/README.md +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/requirements.txt +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/setup.cfg +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/setup.py +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa/build.py +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa/lib.py +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {c2pa_python-0.35.0 → c2pa_python-0.35.2}/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.
|
|
3
|
+
Version: 0.35.2
|
|
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.
|
|
7
|
+
version = "0.35.2"
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1693
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1777
|
+
s._file_like_stream.flush()
|
|
1750
1778
|
return 0
|
|
1751
1779
|
except Exception:
|
|
1752
1780
|
return -1
|
|
@@ -2235,6 +2263,9 @@ class Reader(ManagedResource):
|
|
|
2235
2263
|
if not context.is_valid:
|
|
2236
2264
|
raise C2paError("Context is not valid")
|
|
2237
2265
|
|
|
2266
|
+
if manifest_data is not None and not isinstance(manifest_data, bytes):
|
|
2267
|
+
raise TypeError(Reader._ERROR_MESSAGES['manifest_error'])
|
|
2268
|
+
|
|
2238
2269
|
# Determine format and open stream
|
|
2239
2270
|
supported = Reader.get_supported_mime_types()
|
|
2240
2271
|
|
|
@@ -2275,10 +2306,6 @@ class Reader(ManagedResource):
|
|
|
2275
2306
|
raise
|
|
2276
2307
|
|
|
2277
2308
|
if manifest_data is not None:
|
|
2278
|
-
if not isinstance(manifest_data, bytes):
|
|
2279
|
-
raise TypeError(
|
|
2280
|
-
Reader._ERROR_MESSAGES[
|
|
2281
|
-
'manifest_error'])
|
|
2282
2309
|
manifest_array = (
|
|
2283
2310
|
ctypes.c_ubyte *
|
|
2284
2311
|
len(manifest_data)).from_buffer_copy(manifest_data)
|
|
@@ -2294,14 +2321,15 @@ class Reader(ManagedResource):
|
|
|
2294
2321
|
len(manifest_data),
|
|
2295
2322
|
)
|
|
2296
2323
|
)
|
|
2297
|
-
# reader_ptr has been invalidated(consumed)
|
|
2298
2324
|
else:
|
|
2299
2325
|
# Consume reader with stream
|
|
2300
2326
|
new_ptr = _lib.c2pa_reader_with_stream(
|
|
2301
2327
|
reader_ptr, format_bytes,
|
|
2302
2328
|
self._own_stream._stream,
|
|
2303
2329
|
)
|
|
2304
|
-
|
|
2330
|
+
|
|
2331
|
+
# reader_ptr has been consumed by the FFI call.
|
|
2332
|
+
reader_ptr = None
|
|
2305
2333
|
|
|
2306
2334
|
self._handle = new_ptr
|
|
2307
2335
|
|
|
@@ -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.
|
|
3
|
+
Version: 0.35.2
|
|
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)
|
|
@@ -5548,6 +6041,119 @@ class TestReaderWithContext(TestContextAPIs):
|
|
|
5548
6041
|
context.close()
|
|
5549
6042
|
|
|
5550
6043
|
|
|
6044
|
+
class TestSidecarReader(TestContextAPIs):
|
|
6045
|
+
"""Reader over a detached (sidecar) manifest with a Context"""
|
|
6046
|
+
|
|
6047
|
+
def _make_detached_manifest(self):
|
|
6048
|
+
"""Sign DEFAULT_TEST_FILE with no-embed, return (asset_bytes,
|
|
6049
|
+
manifest_bytes) where manifest_bytes is the "detached" sidecar
|
|
6050
|
+
C2PA manifest."""
|
|
6051
|
+
|
|
6052
|
+
signer = self._ctx_make_signer()
|
|
6053
|
+
with open(DEFAULT_TEST_FILE, "rb") as f:
|
|
6054
|
+
asset_bytes = f.read()
|
|
6055
|
+
builder = Builder(self.test_manifest)
|
|
6056
|
+
builder.set_no_embed()
|
|
6057
|
+
# Output is discarded: with no-embed the asset is unchanged and the
|
|
6058
|
+
# manifest is returned by sign().
|
|
6059
|
+
manifest_bytes = builder.sign(
|
|
6060
|
+
signer, "image/jpeg", io.BytesIO(asset_bytes), io.BytesIO())
|
|
6061
|
+
builder.close()
|
|
6062
|
+
signer.close()
|
|
6063
|
+
self.assertIsInstance(manifest_bytes, bytes)
|
|
6064
|
+
self.assertGreater(len(manifest_bytes), 0)
|
|
6065
|
+
return asset_bytes, manifest_bytes
|
|
6066
|
+
|
|
6067
|
+
def test_reader_with_manifest_data_and_context(self):
|
|
6068
|
+
asset_bytes, manifest_bytes = self._make_detached_manifest()
|
|
6069
|
+
context = Context()
|
|
6070
|
+
reader = Reader(
|
|
6071
|
+
"image/jpeg",
|
|
6072
|
+
io.BytesIO(asset_bytes),
|
|
6073
|
+
manifest_bytes,
|
|
6074
|
+
context=context,
|
|
6075
|
+
)
|
|
6076
|
+
try:
|
|
6077
|
+
data = reader.json()
|
|
6078
|
+
self.assertTrue(data)
|
|
6079
|
+
self.assertFalse(reader.is_embedded())
|
|
6080
|
+
self.assertIn("manifests", json.loads(data))
|
|
6081
|
+
finally:
|
|
6082
|
+
reader.close()
|
|
6083
|
+
context.close()
|
|
6084
|
+
|
|
6085
|
+
def test_reader_manifest_data_context_invalid_manifest_raises(self):
|
|
6086
|
+
with open(DEFAULT_TEST_FILE, "rb") as f:
|
|
6087
|
+
asset_bytes = f.read()
|
|
6088
|
+
context = Context()
|
|
6089
|
+
reader = None
|
|
6090
|
+
try:
|
|
6091
|
+
with self.assertRaises(Error):
|
|
6092
|
+
reader = Reader(
|
|
6093
|
+
"image/jpeg",
|
|
6094
|
+
io.BytesIO(asset_bytes),
|
|
6095
|
+
b"not a real manifest",
|
|
6096
|
+
context=context,
|
|
6097
|
+
)
|
|
6098
|
+
# The consumed-pointer error branch must leave no usable handle.
|
|
6099
|
+
if reader is not None:
|
|
6100
|
+
self.assertEqual(
|
|
6101
|
+
reader._lifecycle_state, LifecycleState.CLOSED)
|
|
6102
|
+
self.assertIsNone(reader._handle)
|
|
6103
|
+
finally:
|
|
6104
|
+
if reader is not None:
|
|
6105
|
+
reader.close()
|
|
6106
|
+
context.close()
|
|
6107
|
+
|
|
6108
|
+
def test_reader_manifest_data_context_wrong_type_raises(self):
|
|
6109
|
+
with open(DEFAULT_TEST_FILE, "rb") as f:
|
|
6110
|
+
asset_bytes = f.read()
|
|
6111
|
+
context = Context()
|
|
6112
|
+
try:
|
|
6113
|
+
# Non-bytes manifest_data must raise TypeError before any native
|
|
6114
|
+
# reader handle is allocated (no leak on this path).
|
|
6115
|
+
with self.assertRaises(TypeError):
|
|
6116
|
+
Reader(
|
|
6117
|
+
"image/jpeg",
|
|
6118
|
+
io.BytesIO(asset_bytes),
|
|
6119
|
+
"manifest as str, not bytes",
|
|
6120
|
+
context=context,
|
|
6121
|
+
)
|
|
6122
|
+
finally:
|
|
6123
|
+
context.close()
|
|
6124
|
+
|
|
6125
|
+
def test_reader_manifest_data_context_use_after_close_raises(self):
|
|
6126
|
+
asset_bytes, manifest_bytes = self._make_detached_manifest()
|
|
6127
|
+
context = Context()
|
|
6128
|
+
reader = Reader(
|
|
6129
|
+
"image/jpeg",
|
|
6130
|
+
io.BytesIO(asset_bytes),
|
|
6131
|
+
manifest_bytes,
|
|
6132
|
+
context=context,
|
|
6133
|
+
)
|
|
6134
|
+
reader.close()
|
|
6135
|
+
self.assertEqual(reader._lifecycle_state, LifecycleState.CLOSED)
|
|
6136
|
+
with self.assertRaises(Error):
|
|
6137
|
+
reader.json()
|
|
6138
|
+
# Idempotent close after use-after-close attempt.
|
|
6139
|
+
reader.close()
|
|
6140
|
+
context.close()
|
|
6141
|
+
|
|
6142
|
+
def test_reader_manifest_data_context_as_context_manager(self):
|
|
6143
|
+
asset_bytes, manifest_bytes = self._make_detached_manifest()
|
|
6144
|
+
context = Context()
|
|
6145
|
+
with Reader(
|
|
6146
|
+
"image/jpeg",
|
|
6147
|
+
io.BytesIO(asset_bytes),
|
|
6148
|
+
manifest_bytes,
|
|
6149
|
+
context=context,
|
|
6150
|
+
) as reader:
|
|
6151
|
+
self.assertEqual(reader._lifecycle_state, LifecycleState.ACTIVE)
|
|
6152
|
+
self.assertTrue(reader.json())
|
|
6153
|
+
self.assertEqual(reader._lifecycle_state, LifecycleState.CLOSED)
|
|
6154
|
+
context.close()
|
|
6155
|
+
|
|
6156
|
+
|
|
5551
6157
|
class TestBuilderWithContext(TestContextAPIs):
|
|
5552
6158
|
|
|
5553
6159
|
def test_contextual_builder_with_default_context(self):
|
|
@@ -6255,5 +6861,68 @@ class TestContextIntegration(TestContextAPIs):
|
|
|
6255
6861
|
context.close()
|
|
6256
6862
|
|
|
6257
6863
|
|
|
6864
|
+
class TestStreamReferences(unittest.TestCase):
|
|
6865
|
+
|
|
6866
|
+
def test_stream_collected_after_del(self):
|
|
6867
|
+
"""Stream must be collected by reference counting."""
|
|
6868
|
+
import gc
|
|
6869
|
+
import weakref
|
|
6870
|
+
gc.collect()
|
|
6871
|
+
garbage_before = len(gc.garbage)
|
|
6872
|
+
|
|
6873
|
+
buf = io.BytesIO(b"hello world")
|
|
6874
|
+
s = Stream(buf)
|
|
6875
|
+
ref = weakref.ref(s)
|
|
6876
|
+
del s
|
|
6877
|
+
|
|
6878
|
+
# Trigger gc, we want to verify it's collected
|
|
6879
|
+
# collected now (1 gc call) means no gc cycle breaker needed
|
|
6880
|
+
# aka ref cycle did not happen.
|
|
6881
|
+
gc.collect()
|
|
6882
|
+
|
|
6883
|
+
self.assertIsNone(ref(), "Stream not collected")
|
|
6884
|
+
self.assertEqual(len(gc.garbage), garbage_before,
|
|
6885
|
+
"Stream added objects to gc.garbage")
|
|
6886
|
+
|
|
6887
|
+
def test_stream_not_added_to_gc_garbage_list(self):
|
|
6888
|
+
"""Creating and dropping many Streams must not grow gc.garbage."""
|
|
6889
|
+
import gc
|
|
6890
|
+
gc.collect()
|
|
6891
|
+
gc.garbage.clear()
|
|
6892
|
+
|
|
6893
|
+
for _ in range(20):
|
|
6894
|
+
s = Stream(io.BytesIO(b"data"))
|
|
6895
|
+
del s
|
|
6896
|
+
|
|
6897
|
+
gc.collect()
|
|
6898
|
+
self.assertEqual(len(gc.garbage), 0,
|
|
6899
|
+
f"gc.garbage unexpectedly non-empty: {gc.garbage}")
|
|
6900
|
+
|
|
6901
|
+
def test_callbacks_return_minus_one_after_stream_collected(self):
|
|
6902
|
+
"""Callbacks must return -1 gracefully when the Stream has been GC'd."""
|
|
6903
|
+
import gc
|
|
6904
|
+
import weakref
|
|
6905
|
+
|
|
6906
|
+
buf = io.BytesIO(b"test")
|
|
6907
|
+
s = Stream(buf)
|
|
6908
|
+
|
|
6909
|
+
read_cb = s._read_cb
|
|
6910
|
+
seek_cb = s._seek_cb
|
|
6911
|
+
write_cb = s._write_cb
|
|
6912
|
+
flush_cb = s._flush_cb
|
|
6913
|
+
ref = weakref.ref(s)
|
|
6914
|
+
del s
|
|
6915
|
+
gc.collect()
|
|
6916
|
+
|
|
6917
|
+
# Stream should be gone.
|
|
6918
|
+
self.assertIsNone(ref(), "Stream not collected before callback test")
|
|
6919
|
+
|
|
6920
|
+
# All callbacks must return -1 without crashing.
|
|
6921
|
+
self.assertEqual(read_cb(None, None, 0), -1)
|
|
6922
|
+
self.assertEqual(seek_cb(None, 0, 0), -1)
|
|
6923
|
+
self.assertEqual(write_cb(None, None, 0), -1)
|
|
6924
|
+
self.assertEqual(flush_cb(None), -1)
|
|
6925
|
+
|
|
6926
|
+
|
|
6258
6927
|
if __name__ == '__main__':
|
|
6259
6928
|
unittest.main(warnings='ignore')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|