c2pa-python 0.32.14__tar.gz → 0.35.0__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.32.14/src/c2pa_python.egg-info → c2pa_python-0.35.0}/PKG-INFO +1 -1
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/pyproject.toml +1 -1
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/scripts/download_artifacts.py +1 -1
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/setup.py +2 -1
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa/__init__.py +0 -2
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa/c2pa.py +67 -283
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa/lib.py +30 -2
- {c2pa_python-0.32.14 → c2pa_python-0.35.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/tests/test_unit_tests.py +1 -487
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/tests/test_unit_tests_threaded.py +161 -1
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/LICENSE-APACHE +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/LICENSE-MIT +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/MANIFEST.in +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/README.md +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/requirements.txt +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/setup.cfg +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa/build.py +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.32.14 → c2pa_python-0.35.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
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
6
|
Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "c2pa-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.35.0"
|
|
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" }
|
|
@@ -91,7 +91,7 @@ def get_platform_identifier(target_arch=None):
|
|
|
91
91
|
else:
|
|
92
92
|
return "universal-apple-darwin"
|
|
93
93
|
elif system == "windows":
|
|
94
|
-
if target_arch == "arm64":
|
|
94
|
+
if target_arch == "arm64" or machine in ["arm64", "aarch64"]:
|
|
95
95
|
return "aarch64-pc-windows-msvc"
|
|
96
96
|
else:
|
|
97
97
|
return "x86_64-pc-windows-msvc"
|
|
@@ -41,6 +41,7 @@ PLATFORM_FOLDERS = {
|
|
|
41
41
|
'aarch64-apple-darwin': 'dylib',
|
|
42
42
|
'x86_64-apple-darwin': 'dylib',
|
|
43
43
|
'x86_64-pc-windows-msvc': 'dll',
|
|
44
|
+
'aarch64-pc-windows-msvc': 'dll',
|
|
44
45
|
'x86_64-unknown-linux-gnu': 'so',
|
|
45
46
|
'aarch64-unknown-linux-gnu': 'so',
|
|
46
47
|
}
|
|
@@ -78,7 +79,7 @@ def get_platform_identifier(target_arch=None) -> str:
|
|
|
78
79
|
else:
|
|
79
80
|
return "universal-apple-darwin"
|
|
80
81
|
elif system == "windows":
|
|
81
|
-
if target_arch == "arm64":
|
|
82
|
+
if target_arch == "arm64" or platform.machine().lower() in ["arm64", "aarch64"]:
|
|
82
83
|
return "aarch64-pc-windows-msvc"
|
|
83
84
|
else:
|
|
84
85
|
return "x86_64-pc-windows-msvc"
|
|
@@ -32,7 +32,6 @@ from .c2pa import (
|
|
|
32
32
|
ContextBuilder,
|
|
33
33
|
ContextProvider,
|
|
34
34
|
sdk_version,
|
|
35
|
-
read_ingredient_file,
|
|
36
35
|
load_settings
|
|
37
36
|
) # NOQA
|
|
38
37
|
|
|
@@ -52,6 +51,5 @@ __all__ = [
|
|
|
52
51
|
'ContextBuilder',
|
|
53
52
|
'ContextProvider',
|
|
54
53
|
'sdk_version',
|
|
55
|
-
'read_ingredient_file',
|
|
56
54
|
'load_settings'
|
|
57
55
|
]
|
|
@@ -22,7 +22,7 @@ from abc import ABC, abstractmethod
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Optional, Union, Callable, Any, overload
|
|
24
24
|
import io
|
|
25
|
-
from .lib import dynamically_load_library
|
|
25
|
+
from .lib import dynamically_load_library, record_owner_pid, is_foreign_process
|
|
26
26
|
import mimetypes
|
|
27
27
|
from itertools import count
|
|
28
28
|
|
|
@@ -38,8 +38,6 @@ _REQUIRED_FUNCTIONS = [
|
|
|
38
38
|
'c2pa_error',
|
|
39
39
|
# Legacy APIs, deprecated
|
|
40
40
|
'c2pa_load_settings',
|
|
41
|
-
'c2pa_read_file',
|
|
42
|
-
'c2pa_read_ingredient_file',
|
|
43
41
|
# Stream
|
|
44
42
|
'c2pa_create_stream',
|
|
45
43
|
'c2pa_release_stream',
|
|
@@ -240,11 +238,17 @@ class ManagedResource:
|
|
|
240
238
|
self._lifecycle_state = LifecycleState.UNINITIALIZED
|
|
241
239
|
self._handle = None
|
|
242
240
|
_clear_error_state()
|
|
241
|
+
record_owner_pid(self)
|
|
243
242
|
|
|
244
243
|
@staticmethod
|
|
245
244
|
def _free_native_ptr(ptr):
|
|
246
|
-
"""Free a native pointer by
|
|
247
|
-
|
|
245
|
+
"""Free a native pointer by passing it to c2pa_free.
|
|
246
|
+
|
|
247
|
+
c2pa_free's argtype is c_void_p, so ctypes converts any pointer
|
|
248
|
+
instance directly. (ctypes.cast(ptr, c_void_p) would do the same
|
|
249
|
+
conversion but leaves a reference cycle behind on every call.)
|
|
250
|
+
"""
|
|
251
|
+
_lib.c2pa_free(ptr)
|
|
248
252
|
|
|
249
253
|
def _ensure_valid_state(self):
|
|
250
254
|
"""Raise if the resource is closed or uninitialized."""
|
|
@@ -277,6 +281,8 @@ class ManagedResource:
|
|
|
277
281
|
def _cleanup_resources(self):
|
|
278
282
|
"""Release native resources idempotently."""
|
|
279
283
|
try:
|
|
284
|
+
if is_foreign_process(self):
|
|
285
|
+
return
|
|
280
286
|
if (
|
|
281
287
|
hasattr(self, '_lifecycle_state')
|
|
282
288
|
and self._lifecycle_state != LifecycleState.CLOSED
|
|
@@ -645,12 +651,6 @@ _setup_function(_lib.c2pa_signer_create,
|
|
|
645
651
|
_setup_function(_lib.c2pa_signer_from_info,
|
|
646
652
|
[ctypes.POINTER(C2paSignerInfo)],
|
|
647
653
|
ctypes.POINTER(C2paSigner))
|
|
648
|
-
_setup_function(
|
|
649
|
-
_lib.c2pa_read_file, [
|
|
650
|
-
ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
|
|
651
|
-
_setup_function(
|
|
652
|
-
_lib.c2pa_read_ingredient_file, [
|
|
653
|
-
ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
|
|
654
654
|
|
|
655
655
|
# Set up Signer function prototypes
|
|
656
656
|
_setup_function(
|
|
@@ -889,33 +889,51 @@ class _StringContainer:
|
|
|
889
889
|
self._data_dir_str = ""
|
|
890
890
|
|
|
891
891
|
|
|
892
|
+
if sys.implementation.name == "cpython":
|
|
893
|
+
_PyMemoryView_FromMemory = ctypes.pythonapi.PyMemoryView_FromMemory
|
|
894
|
+
_PyMemoryView_FromMemory.restype = ctypes.py_object
|
|
895
|
+
_PyMemoryView_FromMemory.argtypes = (
|
|
896
|
+
ctypes.c_void_p, ctypes.c_ssize_t, ctypes.c_int)
|
|
897
|
+
_PyBUF_WRITE = 0x200
|
|
898
|
+
|
|
899
|
+
def _write_buf(address, length):
|
|
900
|
+
return _PyMemoryView_FromMemory(address, length, _PyBUF_WRITE)
|
|
901
|
+
else:
|
|
902
|
+
def _write_buf(address, length):
|
|
903
|
+
return (ctypes.c_char * length).from_address(address)
|
|
904
|
+
|
|
905
|
+
|
|
892
906
|
def _convert_to_py_string(value) -> str:
|
|
893
907
|
if value is None:
|
|
894
908
|
return ""
|
|
895
909
|
|
|
896
910
|
py_string = ""
|
|
897
911
|
|
|
898
|
-
# Validate pointer before
|
|
899
|
-
if
|
|
912
|
+
# Validate and normalize pointer before reading and freeing.
|
|
913
|
+
if isinstance(value, ctypes.c_void_p):
|
|
914
|
+
address = value.value
|
|
915
|
+
elif isinstance(value, int):
|
|
916
|
+
address = value
|
|
917
|
+
else:
|
|
918
|
+
return ""
|
|
919
|
+
if not address:
|
|
900
920
|
return ""
|
|
901
921
|
|
|
902
922
|
try:
|
|
903
|
-
|
|
923
|
+
raw = ctypes.string_at(address)
|
|
904
924
|
|
|
905
|
-
|
|
906
|
-
|
|
925
|
+
try:
|
|
926
|
+
py_string = raw.decode('utf-8', errors='strict')
|
|
927
|
+
except Exception:
|
|
928
|
+
py_string = ""
|
|
929
|
+
finally:
|
|
930
|
+
# Only free if we have a valid pointer
|
|
907
931
|
try:
|
|
908
|
-
|
|
932
|
+
_lib.c2pa_string_free(value)
|
|
909
933
|
except Exception:
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
try:
|
|
914
|
-
_lib.c2pa_string_free(value)
|
|
915
|
-
except Exception:
|
|
916
|
-
# Ignore clean up issues
|
|
917
|
-
pass
|
|
918
|
-
except (ctypes.ArgumentError, TypeError, ValueError):
|
|
934
|
+
# Ignore clean up issues
|
|
935
|
+
pass
|
|
936
|
+
except (ctypes.ArgumentError, TypeError, ValueError, OSError):
|
|
919
937
|
# Invalid pointer type or value
|
|
920
938
|
return ""
|
|
921
939
|
|
|
@@ -1003,8 +1021,7 @@ def _parse_operation_result_for_error(
|
|
|
1003
1021
|
if check_error:
|
|
1004
1022
|
error = _lib.c2pa_error()
|
|
1005
1023
|
if error:
|
|
1006
|
-
error_str = ctypes.
|
|
1007
|
-
error, ctypes.c_char_p).value.decode('utf-8')
|
|
1024
|
+
error_str = ctypes.string_at(error).decode('utf-8')
|
|
1008
1025
|
_lib.c2pa_string_free(error)
|
|
1009
1026
|
_raise_typed_c2pa_error(error_str)
|
|
1010
1027
|
return None
|
|
@@ -1191,208 +1208,6 @@ def _get_mime_type_from_path(path: Union[str, Path]) -> str:
|
|
|
1191
1208
|
return mime_type
|
|
1192
1209
|
|
|
1193
1210
|
|
|
1194
|
-
def read_ingredient_file(
|
|
1195
|
-
path: Union[str, Path], data_dir: Union[str, Path]) -> str:
|
|
1196
|
-
"""Read a file as C2PA ingredient (deprecated).
|
|
1197
|
-
This creates the JSON string that would be used as the ingredient JSON.
|
|
1198
|
-
|
|
1199
|
-
.. deprecated:: 0.11.0
|
|
1200
|
-
This function is deprecated and will be removed in a future version.
|
|
1201
|
-
To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class.
|
|
1202
|
-
To add ingredients to a manifest,
|
|
1203
|
-
use :meth:`c2pa.c2pa.Builder.add_ingredient` instead.
|
|
1204
|
-
|
|
1205
|
-
Args:
|
|
1206
|
-
path: Path to the file to read
|
|
1207
|
-
data_dir: Directory to write binary resources to
|
|
1208
|
-
|
|
1209
|
-
Returns:
|
|
1210
|
-
The ingredient as a JSON string
|
|
1211
|
-
|
|
1212
|
-
Raises:
|
|
1213
|
-
C2paError: If there was an error reading the file
|
|
1214
|
-
"""
|
|
1215
|
-
warnings.warn(
|
|
1216
|
-
"The read_ingredient_file function is deprecated and will be "
|
|
1217
|
-
"removed in a future version. Please use Reader(path).json() for "
|
|
1218
|
-
"reading C2PA metadata instead, or "
|
|
1219
|
-
"Builder.add_ingredient(json, format, stream) to add ingredients "
|
|
1220
|
-
"to a manifest.",
|
|
1221
|
-
DeprecationWarning,
|
|
1222
|
-
stacklevel=2,
|
|
1223
|
-
)
|
|
1224
|
-
|
|
1225
|
-
_clear_error_state()
|
|
1226
|
-
|
|
1227
|
-
container = _StringContainer()
|
|
1228
|
-
|
|
1229
|
-
container._path_str = str(path).encode('utf-8')
|
|
1230
|
-
container._data_dir_str = str(data_dir).encode('utf-8')
|
|
1231
|
-
|
|
1232
|
-
result = _lib.c2pa_read_ingredient_file(
|
|
1233
|
-
container._path_str, container._data_dir_str)
|
|
1234
|
-
|
|
1235
|
-
_check_ffi_operation_result(
|
|
1236
|
-
result, "Error reading ingredient file {}".format(path))
|
|
1237
|
-
|
|
1238
|
-
return _convert_to_py_string(result)
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
def read_file(path: Union[str, Path],
|
|
1242
|
-
data_dir: Union[str, Path]) -> str:
|
|
1243
|
-
"""Read a C2PA manifest from a file (deprecated).
|
|
1244
|
-
|
|
1245
|
-
.. deprecated:: 0.10.0
|
|
1246
|
-
This function is deprecated and will be removed in a future version.
|
|
1247
|
-
To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class.
|
|
1248
|
-
|
|
1249
|
-
Args:
|
|
1250
|
-
path: Path to the file to read
|
|
1251
|
-
data_dir: Directory to write binary resources to
|
|
1252
|
-
|
|
1253
|
-
Returns:
|
|
1254
|
-
The manifest as a JSON string
|
|
1255
|
-
|
|
1256
|
-
Raises:
|
|
1257
|
-
C2paError: If there was an error reading the file
|
|
1258
|
-
"""
|
|
1259
|
-
warnings.warn(
|
|
1260
|
-
"The read_file function is deprecated and will be removed in a "
|
|
1261
|
-
"future version. Please use the Reader class for reading C2PA "
|
|
1262
|
-
"metadata instead.",
|
|
1263
|
-
DeprecationWarning,
|
|
1264
|
-
stacklevel=2,
|
|
1265
|
-
)
|
|
1266
|
-
|
|
1267
|
-
_clear_error_state()
|
|
1268
|
-
|
|
1269
|
-
container = _StringContainer()
|
|
1270
|
-
|
|
1271
|
-
container._path_str = str(path).encode('utf-8')
|
|
1272
|
-
container._data_dir_str = str(data_dir).encode('utf-8')
|
|
1273
|
-
|
|
1274
|
-
result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
|
|
1275
|
-
_check_ffi_operation_result(
|
|
1276
|
-
result, "Error during read of manifest from file {}".format(path))
|
|
1277
|
-
|
|
1278
|
-
return _convert_to_py_string(result)
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
@overload
|
|
1282
|
-
def sign_file(
|
|
1283
|
-
source_path: Union[str, Path],
|
|
1284
|
-
dest_path: Union[str, Path],
|
|
1285
|
-
manifest: str,
|
|
1286
|
-
signer_info: C2paSignerInfo,
|
|
1287
|
-
return_manifest_as_bytes: bool = False
|
|
1288
|
-
) -> Union[str, bytes]:
|
|
1289
|
-
"""Sign a file with a C2PA manifest using signer info.
|
|
1290
|
-
"""
|
|
1291
|
-
...
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
@overload
|
|
1295
|
-
def sign_file(
|
|
1296
|
-
source_path: Union[str, Path],
|
|
1297
|
-
dest_path: Union[str, Path],
|
|
1298
|
-
manifest: str,
|
|
1299
|
-
signer: 'Signer',
|
|
1300
|
-
return_manifest_as_bytes: bool = False
|
|
1301
|
-
) -> Union[str, bytes]:
|
|
1302
|
-
"""Sign a file with a C2PA manifest using a signer.
|
|
1303
|
-
"""
|
|
1304
|
-
...
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
def sign_file(
|
|
1308
|
-
source_path: Union[str, Path],
|
|
1309
|
-
dest_path: Union[str, Path],
|
|
1310
|
-
manifest: str,
|
|
1311
|
-
signer_or_info: Union[C2paSignerInfo, 'Signer'],
|
|
1312
|
-
return_manifest_as_bytes: bool = False
|
|
1313
|
-
) -> Union[str, bytes]:
|
|
1314
|
-
"""Sign a file with a C2PA manifest (deprecated).
|
|
1315
|
-
For now, this function is left here to provide a backwards-compatible API.
|
|
1316
|
-
|
|
1317
|
-
.. deprecated:: 0.13.0
|
|
1318
|
-
This function is deprecated and will be removed in a future version.
|
|
1319
|
-
Use :meth:`Builder.sign` instead.
|
|
1320
|
-
|
|
1321
|
-
Args:
|
|
1322
|
-
source_path: Path to the source file. We will attempt
|
|
1323
|
-
to guess the mimetype of the source file based on
|
|
1324
|
-
the extension.
|
|
1325
|
-
dest_path: Path to write the signed file to
|
|
1326
|
-
manifest: The manifest JSON string
|
|
1327
|
-
signer_or_info: Either a signer configuration or a signer object
|
|
1328
|
-
return_manifest_as_bytes: If True, return manifest bytes instead
|
|
1329
|
-
of JSON string
|
|
1330
|
-
|
|
1331
|
-
Returns:
|
|
1332
|
-
The signed manifest as a JSON string or bytes, depending
|
|
1333
|
-
on return_manifest_as_bytes
|
|
1334
|
-
|
|
1335
|
-
Raises:
|
|
1336
|
-
C2paError: If there was an error signing the file
|
|
1337
|
-
C2paError.Encoding: If any of the string inputs contain
|
|
1338
|
-
invalid UTF-8 characters
|
|
1339
|
-
C2paError.NotSupported: If the file type cannot be determined
|
|
1340
|
-
"""
|
|
1341
|
-
|
|
1342
|
-
warnings.warn(
|
|
1343
|
-
"The sign_file function is deprecated and will be removed in a "
|
|
1344
|
-
"future version. Please use the Builder object and Builder.sign() "
|
|
1345
|
-
"instead.",
|
|
1346
|
-
DeprecationWarning,
|
|
1347
|
-
stacklevel=2,
|
|
1348
|
-
)
|
|
1349
|
-
|
|
1350
|
-
_clear_error_state()
|
|
1351
|
-
|
|
1352
|
-
try:
|
|
1353
|
-
# Determine if we have a signer or signer info
|
|
1354
|
-
if isinstance(signer_or_info, C2paSignerInfo):
|
|
1355
|
-
signer = Signer.from_info(signer_or_info)
|
|
1356
|
-
own_signer = True
|
|
1357
|
-
else:
|
|
1358
|
-
signer = signer_or_info
|
|
1359
|
-
own_signer = False
|
|
1360
|
-
|
|
1361
|
-
# Create a builder from the manifest
|
|
1362
|
-
builder = Builder(manifest)
|
|
1363
|
-
|
|
1364
|
-
manifest_bytes = builder.sign_file(
|
|
1365
|
-
source_path,
|
|
1366
|
-
dest_path,
|
|
1367
|
-
signer
|
|
1368
|
-
)
|
|
1369
|
-
|
|
1370
|
-
if return_manifest_as_bytes:
|
|
1371
|
-
return manifest_bytes
|
|
1372
|
-
else:
|
|
1373
|
-
# Read the signed manifest from the destination file
|
|
1374
|
-
with Reader(dest_path) as reader:
|
|
1375
|
-
return reader.json()
|
|
1376
|
-
|
|
1377
|
-
except Exception as e:
|
|
1378
|
-
# Clean up destination file if it exists and there was an error
|
|
1379
|
-
if os.path.exists(dest_path):
|
|
1380
|
-
try:
|
|
1381
|
-
os.remove(dest_path)
|
|
1382
|
-
except OSError:
|
|
1383
|
-
logger.warning("Failed to remove destination file")
|
|
1384
|
-
pass # Ignore cleanup errors
|
|
1385
|
-
|
|
1386
|
-
# Re-raise the error
|
|
1387
|
-
raise C2paError(f"Error signing file: {str(e)}") from e
|
|
1388
|
-
finally:
|
|
1389
|
-
# Ensure resources are cleaned up
|
|
1390
|
-
if 'builder' in locals():
|
|
1391
|
-
builder.close()
|
|
1392
|
-
if 'signer' in locals() and own_signer:
|
|
1393
|
-
signer.close()
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
1211
|
class ContextProvider(ABC):
|
|
1397
1212
|
"""Abstract base class for types that provide a C2PA context.
|
|
1398
1213
|
|
|
@@ -1827,10 +1642,21 @@ class Stream:
|
|
|
1827
1642
|
readinto = getattr(stream, "readinto", None)
|
|
1828
1643
|
if readinto is not None:
|
|
1829
1644
|
# Most streams have readinto
|
|
1830
|
-
buf = (
|
|
1831
|
-
ctypes.addressof(data.contents))
|
|
1832
|
-
|
|
1833
|
-
|
|
1645
|
+
buf = _write_buf(
|
|
1646
|
+
ctypes.addressof(data.contents), length)
|
|
1647
|
+
try:
|
|
1648
|
+
n = readinto(buf)
|
|
1649
|
+
finally:
|
|
1650
|
+
release = getattr(buf, "release", None)
|
|
1651
|
+
if release is not None:
|
|
1652
|
+
release()
|
|
1653
|
+
if not n:
|
|
1654
|
+
return 0
|
|
1655
|
+
if n > length:
|
|
1656
|
+
raise ValueError(
|
|
1657
|
+
f"readinto returned {n} bytes but buffer length is {length}"
|
|
1658
|
+
)
|
|
1659
|
+
return n
|
|
1834
1660
|
|
|
1835
1661
|
# Fallback for streams without readinto.
|
|
1836
1662
|
buffer = stream.read(length)
|
|
@@ -1945,6 +1771,7 @@ class Stream:
|
|
|
1945
1771
|
raise Exception("Failed to create stream: {}".format(error))
|
|
1946
1772
|
|
|
1947
1773
|
self._initialized = True
|
|
1774
|
+
record_owner_pid(self)
|
|
1948
1775
|
|
|
1949
1776
|
def __enter__(self):
|
|
1950
1777
|
"""Context manager entry."""
|
|
@@ -1963,6 +1790,8 @@ class Stream:
|
|
|
1963
1790
|
hasn't been explicitly closed.
|
|
1964
1791
|
"""
|
|
1965
1792
|
try:
|
|
1793
|
+
if is_foreign_process(self):
|
|
1794
|
+
return
|
|
1966
1795
|
# Only cleanup if not already closed and we have a valid stream
|
|
1967
1796
|
if hasattr(self, '_closed') and not self._closed:
|
|
1968
1797
|
stream = self._stream
|
|
@@ -1993,6 +1822,10 @@ class Stream:
|
|
|
1993
1822
|
"""
|
|
1994
1823
|
if self._closed:
|
|
1995
1824
|
return
|
|
1825
|
+
if is_foreign_process(self):
|
|
1826
|
+
self._closed = True
|
|
1827
|
+
self._initialized = False
|
|
1828
|
+
return
|
|
1996
1829
|
|
|
1997
1830
|
try:
|
|
1998
1831
|
# Clean up stream first as it depends on callbacks
|
|
@@ -3411,52 +3244,6 @@ class Builder(ManagedResource):
|
|
|
3411
3244
|
Builder._ERROR_MESSAGES['ingredient_error'].format("Unknown error"),
|
|
3412
3245
|
check=lambda r: r != 0)
|
|
3413
3246
|
|
|
3414
|
-
def add_ingredient_from_file_path(
|
|
3415
|
-
self,
|
|
3416
|
-
ingredient_json: Union[str, dict],
|
|
3417
|
-
format: str,
|
|
3418
|
-
filepath: Union[str, Path]):
|
|
3419
|
-
"""Add an ingredient from a file path to the builder (deprecated).
|
|
3420
|
-
This is a legacy method.
|
|
3421
|
-
|
|
3422
|
-
.. deprecated:: 0.13.0
|
|
3423
|
-
This method is deprecated and will be removed in a future version.
|
|
3424
|
-
Use :meth:`add_ingredient` with a file stream instead.
|
|
3425
|
-
|
|
3426
|
-
Args:
|
|
3427
|
-
ingredient_json: The JSON ingredient definition
|
|
3428
|
-
(either a JSON string or a dictionary)
|
|
3429
|
-
format: The MIME type or extension of the ingredient
|
|
3430
|
-
filepath: The path to the file containing the ingredient data
|
|
3431
|
-
(can be a string or Path object)
|
|
3432
|
-
|
|
3433
|
-
Raises:
|
|
3434
|
-
C2paError: If there was an error adding the ingredient
|
|
3435
|
-
C2paError.Encoding: If the ingredient JSON or format
|
|
3436
|
-
contains invalid UTF-8 characters
|
|
3437
|
-
FileNotFoundError: If the file at the specified path does not exist
|
|
3438
|
-
"""
|
|
3439
|
-
warnings.warn(
|
|
3440
|
-
"add_ingredient_from_file_path is deprecated and will "
|
|
3441
|
-
"be removed in a future version. Use add_ingredient "
|
|
3442
|
-
"with a file stream instead.",
|
|
3443
|
-
DeprecationWarning,
|
|
3444
|
-
stacklevel=2,
|
|
3445
|
-
)
|
|
3446
|
-
|
|
3447
|
-
try:
|
|
3448
|
-
# Convert Path object to string if necessary
|
|
3449
|
-
filepath_str = str(filepath)
|
|
3450
|
-
|
|
3451
|
-
# Does the stream handling to use add_ingredient_from_stream
|
|
3452
|
-
with open(filepath_str, 'rb') as file_stream:
|
|
3453
|
-
self.add_ingredient_from_stream(
|
|
3454
|
-
ingredient_json, format, file_stream)
|
|
3455
|
-
except FileNotFoundError as e:
|
|
3456
|
-
raise C2paError.FileNotFound(f"File not found: {filepath}") from e
|
|
3457
|
-
except Exception as e:
|
|
3458
|
-
raise C2paError.Other(f"Could not add ingredient: {e}") from e
|
|
3459
|
-
|
|
3460
3247
|
def add_action(self, action_json: Union[str, dict]) -> None:
|
|
3461
3248
|
"""Add an action to the builder, that will be placed
|
|
3462
3249
|
in the actions assertion array in the generated manifest.
|
|
@@ -3991,9 +3778,6 @@ __all__ = [
|
|
|
3991
3778
|
'Builder',
|
|
3992
3779
|
'Signer',
|
|
3993
3780
|
'load_settings',
|
|
3994
|
-
'read_file',
|
|
3995
|
-
'read_ingredient_file',
|
|
3996
|
-
'sign_file',
|
|
3997
3781
|
'format_embeddable',
|
|
3998
3782
|
'version',
|
|
3999
3783
|
'sdk_version'
|
|
@@ -37,7 +37,8 @@ def get_platform_identifier() -> str:
|
|
|
37
37
|
- universal-apple-darwin (for Mac universal)
|
|
38
38
|
- aarch64-apple-darwin (for Mac ARM64)
|
|
39
39
|
- x86_64-apple-darwin (for Mac x86_64)
|
|
40
|
-
- x86_64-pc-windows-msvc (for Windows
|
|
40
|
+
- x86_64-pc-windows-msvc (for Windows x64)
|
|
41
|
+
- aarch64-pc-windows-msvc (for Windows ARM64)
|
|
41
42
|
- x86_64-unknown-linux-gnu (for Linux 64-bit)
|
|
42
43
|
- aarch64-unknown-linux-gnu (for Linux ARM)
|
|
43
44
|
"""
|
|
@@ -54,6 +55,8 @@ def get_platform_identifier() -> str:
|
|
|
54
55
|
else:
|
|
55
56
|
return "universal-apple-darwin"
|
|
56
57
|
elif system == "windows":
|
|
58
|
+
if _get_architecture() in [CPUArchitecture.ARM64.value, CPUArchitecture.AARCH64.value]:
|
|
59
|
+
return "aarch64-pc-windows-msvc"
|
|
57
60
|
return "x86_64-pc-windows-msvc"
|
|
58
61
|
elif system == "linux":
|
|
59
62
|
if _get_architecture() in [CPUArchitecture.ARM64.value, CPUArchitecture.AARCH64.value]:
|
|
@@ -81,7 +84,7 @@ def _get_architecture() -> str:
|
|
|
81
84
|
elif sys.platform == "win32":
|
|
82
85
|
# win32 will cover all Windows versions
|
|
83
86
|
# (the 32 is a historical quirk)
|
|
84
|
-
return platform.machine()
|
|
87
|
+
return platform.machine().lower()
|
|
85
88
|
else:
|
|
86
89
|
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
|
87
90
|
|
|
@@ -296,3 +299,28 @@ def dynamically_load_library(
|
|
|
296
299
|
raise RuntimeError(f"Could not find {c2pa_lib_name} in any of the search paths")
|
|
297
300
|
|
|
298
301
|
return c2pa_lib
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def record_owner_pid(obj):
|
|
305
|
+
"""Keep the PID that created this native-handle wrapper
|
|
306
|
+
(call from __init__ as needed).
|
|
307
|
+
"""
|
|
308
|
+
obj._owner_pid = os.getpid()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def is_foreign_process(obj):
|
|
312
|
+
"""Return True when this object is being finalized in a forked child that did not
|
|
313
|
+
create it. After a multithreaded fork(), native mutexes may be held by threads
|
|
314
|
+
absent in the child -> any lock() call deadlocks. Callers must skip native frees
|
|
315
|
+
when this returns True.
|
|
316
|
+
|
|
317
|
+
Skipping the free does not cause a cumulative leak. If the child calls exec() the
|
|
318
|
+
address space is replaced entirely; if it exits, the OS reclaims all process memory.
|
|
319
|
+
Even a long-lived fork child (e.g. a multiprocessing fork-start worker) leaks at most
|
|
320
|
+
the objects inherited at fork time — a one-time, bounded amount reclaimed when the
|
|
321
|
+
child terminates. Objects the child creates itself carry the child's PID and are
|
|
322
|
+
freed normally.
|
|
323
|
+
|
|
324
|
+
Defensive default: if _owner_pid was never set, returns False (no regression)."""
|
|
325
|
+
owner = getattr(obj, '_owner_pid', None)
|
|
326
|
+
return owner is not None and owner != os.getpid()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.0
|
|
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
6
|
Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
|
|
@@ -31,7 +31,7 @@ warnings.simplefilter("ignore", category=DeprecationWarning)
|
|
|
31
31
|
|
|
32
32
|
from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version, C2paBuilderIntent, C2paDigitalSourceType
|
|
33
33
|
from c2pa import Settings, Context, ContextBuilder, ContextProvider
|
|
34
|
-
from c2pa.c2pa import Stream, LifecycleState,
|
|
34
|
+
from c2pa.c2pa import Stream, LifecycleState, load_settings, create_signer, create_signer_from_info, ed25519_sign, format_embeddable
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
PROJECT_PATH = os.getcwd()
|
|
@@ -4787,9 +4787,6 @@ class TestStream(unittest.TestCase):
|
|
|
4787
4787
|
class TestLegacyAPI(unittest.TestCase):
|
|
4788
4788
|
def setUp(self):
|
|
4789
4789
|
# Filter specific deprecation warnings for legacy API tests
|
|
4790
|
-
warnings.filterwarnings("ignore", message="The read_file function is deprecated")
|
|
4791
|
-
warnings.filterwarnings("ignore", message="The sign_file function is deprecated")
|
|
4792
|
-
warnings.filterwarnings("ignore", message="The read_ingredient_file function is deprecated")
|
|
4793
4790
|
warnings.filterwarnings("ignore", message="The create_signer function is deprecated")
|
|
4794
4791
|
warnings.filterwarnings("ignore", message="The create_signer_from_info function is deprecated")
|
|
4795
4792
|
warnings.filterwarnings("ignore", message="load_settings\\(\\) is deprecated")
|
|
@@ -4863,489 +4860,6 @@ class TestLegacyAPI(unittest.TestCase):
|
|
|
4863
4860
|
if os.path.exists(self.temp_data_dir):
|
|
4864
4861
|
shutil.rmtree(self.temp_data_dir)
|
|
4865
4862
|
|
|
4866
|
-
def test_read_ingredient_file(self):
|
|
4867
|
-
"""Test reading a C2PA ingredient from a file."""
|
|
4868
|
-
# Test reading ingredient from file with data_dir
|
|
4869
|
-
temp_data_dir = os.path.join(self.data_dir, "temp_data")
|
|
4870
|
-
os.makedirs(temp_data_dir, exist_ok=True)
|
|
4871
|
-
|
|
4872
|
-
ingredient_json_with_dir = read_ingredient_file(self.testPath, temp_data_dir)
|
|
4873
|
-
|
|
4874
|
-
# Verify some fields
|
|
4875
|
-
ingredient_data = json.loads(ingredient_json_with_dir)
|
|
4876
|
-
self.assertEqual(ingredient_data["title"], DEFAULT_TEST_FILE_NAME)
|
|
4877
|
-
self.assertEqual(ingredient_data["format"], "image/jpeg")
|
|
4878
|
-
self.assertIn("thumbnail", ingredient_data)
|
|
4879
|
-
|
|
4880
|
-
def test_read_ingredient_file_who_has_no_manifest(self):
|
|
4881
|
-
"""Test reading a C2PA ingredient from a file."""
|
|
4882
|
-
# Test reading ingredient from file with data_dir
|
|
4883
|
-
temp_data_dir = os.path.join(self.data_dir, "temp_data")
|
|
4884
|
-
os.makedirs(temp_data_dir, exist_ok=True)
|
|
4885
|
-
|
|
4886
|
-
# Load settings first, before they need to be used
|
|
4887
|
-
load_settings('{"builder": { "thumbnail": {"enabled": false}}}')
|
|
4888
|
-
|
|
4889
|
-
ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir)
|
|
4890
|
-
|
|
4891
|
-
# Verify some fields
|
|
4892
|
-
ingredient_data = json.loads(ingredient_json_with_dir)
|
|
4893
|
-
self.assertEqual(ingredient_data["title"], INGREDIENT_TEST_FILE_NAME)
|
|
4894
|
-
self.assertEqual(ingredient_data["format"], "image/jpeg")
|
|
4895
|
-
self.assertNotIn("thumbnail", ingredient_data)
|
|
4896
|
-
|
|
4897
|
-
# Reset setting
|
|
4898
|
-
load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
|
|
4899
|
-
|
|
4900
|
-
def test_compare_read_ingredient_file_with_builder_added_ingredient(self):
|
|
4901
|
-
"""Test reading a C2PA ingredient from a file."""
|
|
4902
|
-
# Test reading ingredient from file with data_dir
|
|
4903
|
-
temp_data_dir = os.path.join(self.data_dir, "temp_data")
|
|
4904
|
-
os.makedirs(temp_data_dir, exist_ok=True)
|
|
4905
|
-
|
|
4906
|
-
ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir)
|
|
4907
|
-
|
|
4908
|
-
# Ingredient fields from read_ingredient_file
|
|
4909
|
-
ingredient_data = json.loads(ingredient_json_with_dir)
|
|
4910
|
-
|
|
4911
|
-
# Compare with ingredient added by Builder
|
|
4912
|
-
builder = Builder.from_json(self.manifestDefinition)
|
|
4913
|
-
# Only the title is needed (filename), since title not extracted or guessed from filename
|
|
4914
|
-
ingredient_json = '{ "title" : "A.jpg" }'
|
|
4915
|
-
with open(self.testPath2, 'rb') as f:
|
|
4916
|
-
builder.add_ingredient(ingredient_json, "image/jpeg", f)
|
|
4917
|
-
|
|
4918
|
-
with open(self.testPath2, "rb") as file:
|
|
4919
|
-
output = io.BytesIO(bytearray())
|
|
4920
|
-
builder.sign(self.signer, "image/jpeg", file, output)
|
|
4921
|
-
output.seek(0)
|
|
4922
|
-
|
|
4923
|
-
# Get ingredient fields from signed manifest
|
|
4924
|
-
reader = Reader("image/jpeg", output)
|
|
4925
|
-
json_data = reader.json()
|
|
4926
|
-
manifest_data = json.loads(json_data)
|
|
4927
|
-
active_manifest_id = manifest_data["active_manifest"]
|
|
4928
|
-
active_manifest = manifest_data["manifests"][active_manifest_id]
|
|
4929
|
-
only_ingredient = active_manifest["ingredients"][0]
|
|
4930
|
-
|
|
4931
|
-
self.assertEqual(ingredient_data["title"], only_ingredient["title"])
|
|
4932
|
-
self.assertEqual(ingredient_data["format"], only_ingredient["format"])
|
|
4933
|
-
self.assertEqual(ingredient_data["document_id"], only_ingredient["document_id"])
|
|
4934
|
-
self.assertEqual(ingredient_data["instance_id"], only_ingredient["instance_id"])
|
|
4935
|
-
self.assertEqual(ingredient_data["relationship"], only_ingredient["relationship"])
|
|
4936
|
-
|
|
4937
|
-
def test_read_file(self):
|
|
4938
|
-
"""Test reading a C2PA ingredient from a file."""
|
|
4939
|
-
temp_data_dir = os.path.join(self.data_dir, "temp_data")
|
|
4940
|
-
os.makedirs(temp_data_dir, exist_ok=True)
|
|
4941
|
-
|
|
4942
|
-
# self.testPath has C2PA metadata to read
|
|
4943
|
-
file_json_with_dir = read_file(self.testPath, temp_data_dir)
|
|
4944
|
-
|
|
4945
|
-
# Parse the JSON and verify specific fields
|
|
4946
|
-
file_data = json.loads(file_json_with_dir)
|
|
4947
|
-
expected_manifest_id = "contentauth:urn:uuid:c85a2b90-f1a0-4aa4-b17f-f938b475804e"
|
|
4948
|
-
|
|
4949
|
-
# Verify some fields
|
|
4950
|
-
self.assertEqual(file_data["active_manifest"], expected_manifest_id)
|
|
4951
|
-
self.assertIn("manifests", file_data)
|
|
4952
|
-
self.assertIn(expected_manifest_id, file_data["manifests"])
|
|
4953
|
-
|
|
4954
|
-
def test_sign_file(self):
|
|
4955
|
-
"""Test signing a file with C2PA manifest."""
|
|
4956
|
-
# Set up test paths
|
|
4957
|
-
temp_data_dir = os.path.join(self.data_dir, "temp_data")
|
|
4958
|
-
os.makedirs(temp_data_dir, exist_ok=True)
|
|
4959
|
-
output_path = os.path.join(temp_data_dir, "signed_output.jpg")
|
|
4960
|
-
|
|
4961
|
-
# Load test certificates and key
|
|
4962
|
-
with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
|
|
4963
|
-
certs = cert_file.read()
|
|
4964
|
-
with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
|
|
4965
|
-
key = key_file.read()
|
|
4966
|
-
|
|
4967
|
-
# Create signer info
|
|
4968
|
-
signer_info = C2paSignerInfo(
|
|
4969
|
-
alg=b"es256",
|
|
4970
|
-
sign_cert=certs,
|
|
4971
|
-
private_key=key,
|
|
4972
|
-
ta_url=b"http://timestamp.digicert.com"
|
|
4973
|
-
)
|
|
4974
|
-
|
|
4975
|
-
# Create a simple manifest
|
|
4976
|
-
manifest = {
|
|
4977
|
-
"claim_generator": "python_internals_test",
|
|
4978
|
-
"claim_generator_info": [{
|
|
4979
|
-
"name": "python_internals_test",
|
|
4980
|
-
"version": "0.0.1",
|
|
4981
|
-
}],
|
|
4982
|
-
# Claim version has become mandatory for signing v1 claims
|
|
4983
|
-
"claim_version": 1,
|
|
4984
|
-
"format": "image/jpeg",
|
|
4985
|
-
"title": "Python Test Signed Image",
|
|
4986
|
-
"ingredients": [],
|
|
4987
|
-
"assertions": [
|
|
4988
|
-
{
|
|
4989
|
-
"label": "c2pa.actions",
|
|
4990
|
-
"data": {
|
|
4991
|
-
"actions": [
|
|
4992
|
-
{
|
|
4993
|
-
"action": "c2pa.opened"
|
|
4994
|
-
}
|
|
4995
|
-
]
|
|
4996
|
-
}
|
|
4997
|
-
}
|
|
4998
|
-
]
|
|
4999
|
-
}
|
|
5000
|
-
|
|
5001
|
-
# Convert manifest to JSON string
|
|
5002
|
-
manifest_json = json.dumps(manifest)
|
|
5003
|
-
|
|
5004
|
-
try:
|
|
5005
|
-
sign_file(
|
|
5006
|
-
self.testPath,
|
|
5007
|
-
output_path,
|
|
5008
|
-
manifest_json,
|
|
5009
|
-
signer_info
|
|
5010
|
-
)
|
|
5011
|
-
|
|
5012
|
-
finally:
|
|
5013
|
-
# Clean up
|
|
5014
|
-
if os.path.exists(output_path):
|
|
5015
|
-
os.remove(output_path)
|
|
5016
|
-
|
|
5017
|
-
def test_sign_file_does_not_exist_errors(self):
|
|
5018
|
-
"""Test signing a file with C2PA manifest."""
|
|
5019
|
-
# Set up test paths
|
|
5020
|
-
temp_data_dir = os.path.join(self.data_dir, "temp_data")
|
|
5021
|
-
os.makedirs(temp_data_dir, exist_ok=True)
|
|
5022
|
-
output_path = os.path.join(temp_data_dir, "signed_output.jpg")
|
|
5023
|
-
|
|
5024
|
-
# Load test certificates and key
|
|
5025
|
-
with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
|
|
5026
|
-
certs = cert_file.read()
|
|
5027
|
-
with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
|
|
5028
|
-
key = key_file.read()
|
|
5029
|
-
|
|
5030
|
-
# Create signer info
|
|
5031
|
-
signer_info = C2paSignerInfo(
|
|
5032
|
-
alg=b"es256",
|
|
5033
|
-
sign_cert=certs,
|
|
5034
|
-
private_key=key,
|
|
5035
|
-
ta_url=b"http://timestamp.digicert.com"
|
|
5036
|
-
)
|
|
5037
|
-
|
|
5038
|
-
# Create a simple manifest
|
|
5039
|
-
manifest = {
|
|
5040
|
-
"claim_generator": "python_internals_test",
|
|
5041
|
-
"claim_generator_info": [{
|
|
5042
|
-
"name": "python_internals_test",
|
|
5043
|
-
"version": "0.0.1",
|
|
5044
|
-
}],
|
|
5045
|
-
# Claim version has become mandatory for signing v1 claims
|
|
5046
|
-
"claim_version": 1,
|
|
5047
|
-
"format": "image/jpeg",
|
|
5048
|
-
"title": "Python Test Signed Image",
|
|
5049
|
-
"ingredients": [],
|
|
5050
|
-
"assertions": [
|
|
5051
|
-
{
|
|
5052
|
-
"label": "c2pa.actions",
|
|
5053
|
-
"data": {
|
|
5054
|
-
"actions": [
|
|
5055
|
-
{
|
|
5056
|
-
"action": "c2pa.opened"
|
|
5057
|
-
}
|
|
5058
|
-
]
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
]
|
|
5062
|
-
}
|
|
5063
|
-
|
|
5064
|
-
# Convert manifest to JSON string
|
|
5065
|
-
manifest_json = json.dumps(manifest)
|
|
5066
|
-
|
|
5067
|
-
try:
|
|
5068
|
-
with self.assertRaises(Error):
|
|
5069
|
-
sign_file(
|
|
5070
|
-
"this-file-does-not-exist",
|
|
5071
|
-
output_path,
|
|
5072
|
-
manifest_json,
|
|
5073
|
-
signer_info
|
|
5074
|
-
)
|
|
5075
|
-
|
|
5076
|
-
finally:
|
|
5077
|
-
# Clean up
|
|
5078
|
-
if os.path.exists(output_path):
|
|
5079
|
-
os.remove(output_path)
|
|
5080
|
-
|
|
5081
|
-
def test_builder_sign_with_ingredient_from_file(self):
|
|
5082
|
-
"""Test Builder class operations with an ingredient added from file path."""
|
|
5083
|
-
|
|
5084
|
-
builder = Builder.from_json(self.manifestDefinition)
|
|
5085
|
-
|
|
5086
|
-
# Test adding ingredient from file path
|
|
5087
|
-
ingredient_json = '{"title": "Test Ingredient From File"}'
|
|
5088
|
-
# Suppress the specific deprecation warning for this test, as this is a legacy method
|
|
5089
|
-
with warnings.catch_warnings():
|
|
5090
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
|
5091
|
-
builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath3)
|
|
5092
|
-
|
|
5093
|
-
with open(self.testPath2, "rb") as file:
|
|
5094
|
-
output = io.BytesIO(bytearray())
|
|
5095
|
-
builder.sign(self.signer, "image/jpeg", file, output)
|
|
5096
|
-
output.seek(0)
|
|
5097
|
-
reader = Reader("image/jpeg", output)
|
|
5098
|
-
json_data = reader.json()
|
|
5099
|
-
manifest_data = json.loads(json_data)
|
|
5100
|
-
|
|
5101
|
-
# Verify active manifest exists
|
|
5102
|
-
self.assertIn("active_manifest", manifest_data)
|
|
5103
|
-
active_manifest_id = manifest_data["active_manifest"]
|
|
5104
|
-
|
|
5105
|
-
# Verify active manifest object exists
|
|
5106
|
-
self.assertIn("manifests", manifest_data)
|
|
5107
|
-
self.assertIn(active_manifest_id, manifest_data["manifests"])
|
|
5108
|
-
active_manifest = manifest_data["manifests"][active_manifest_id]
|
|
5109
|
-
|
|
5110
|
-
# Verify ingredients array exists in active manifest
|
|
5111
|
-
self.assertIn("ingredients", active_manifest)
|
|
5112
|
-
self.assertIsInstance(active_manifest["ingredients"], list)
|
|
5113
|
-
self.assertTrue(len(active_manifest["ingredients"]) > 0)
|
|
5114
|
-
|
|
5115
|
-
# Verify the first ingredient's title matches what we set
|
|
5116
|
-
first_ingredient = active_manifest["ingredients"][0]
|
|
5117
|
-
self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
|
|
5118
|
-
|
|
5119
|
-
builder.close()
|
|
5120
|
-
|
|
5121
|
-
def test_builder_sign_with_ingredient_dict_from_file(self):
|
|
5122
|
-
"""Test Builder class operations with an ingredient added from file path using a dictionary."""
|
|
5123
|
-
|
|
5124
|
-
builder = Builder.from_json(self.manifestDefinition)
|
|
5125
|
-
|
|
5126
|
-
# Test adding ingredient from file path with a dictionary
|
|
5127
|
-
ingredient_dict = {"title": "Test Ingredient From File"}
|
|
5128
|
-
# Suppress the specific deprecation warning for this test, as this is a legacy method
|
|
5129
|
-
with warnings.catch_warnings():
|
|
5130
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
|
5131
|
-
builder.add_ingredient_from_file_path(ingredient_dict, "image/jpeg", self.testPath3)
|
|
5132
|
-
|
|
5133
|
-
with open(self.testPath2, "rb") as file:
|
|
5134
|
-
output = io.BytesIO(bytearray())
|
|
5135
|
-
builder.sign(self.signer, "image/jpeg", file, output)
|
|
5136
|
-
output.seek(0)
|
|
5137
|
-
reader = Reader("image/jpeg", output)
|
|
5138
|
-
json_data = reader.json()
|
|
5139
|
-
manifest_data = json.loads(json_data)
|
|
5140
|
-
|
|
5141
|
-
# Verify active manifest exists
|
|
5142
|
-
self.assertIn("active_manifest", manifest_data)
|
|
5143
|
-
active_manifest_id = manifest_data["active_manifest"]
|
|
5144
|
-
|
|
5145
|
-
# Verify active manifest object exists
|
|
5146
|
-
self.assertIn("manifests", manifest_data)
|
|
5147
|
-
self.assertIn(active_manifest_id, manifest_data["manifests"])
|
|
5148
|
-
active_manifest = manifest_data["manifests"][active_manifest_id]
|
|
5149
|
-
|
|
5150
|
-
# Verify ingredients array exists in active manifest
|
|
5151
|
-
self.assertIn("ingredients", active_manifest)
|
|
5152
|
-
self.assertIsInstance(active_manifest["ingredients"], list)
|
|
5153
|
-
self.assertTrue(len(active_manifest["ingredients"]) > 0)
|
|
5154
|
-
|
|
5155
|
-
# Verify the first ingredient's title matches what we set
|
|
5156
|
-
first_ingredient = active_manifest["ingredients"][0]
|
|
5157
|
-
self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
|
|
5158
|
-
|
|
5159
|
-
builder.close()
|
|
5160
|
-
|
|
5161
|
-
def test_builder_add_ingredient_from_file_path(self):
|
|
5162
|
-
"""Test Builder class add_ingredient_from_file_path method."""
|
|
5163
|
-
|
|
5164
|
-
# Suppress the specific deprecation warning for this test, as this is a legacy method
|
|
5165
|
-
with warnings.catch_warnings():
|
|
5166
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
|
5167
|
-
|
|
5168
|
-
builder = Builder.from_json(self.manifestDefinition)
|
|
5169
|
-
|
|
5170
|
-
# Test adding ingredient from file path
|
|
5171
|
-
ingredient_json = '{"test": "ingredient_from_file_path"}'
|
|
5172
|
-
builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath)
|
|
5173
|
-
|
|
5174
|
-
builder.close()
|
|
5175
|
-
|
|
5176
|
-
def test_builder_add_ingredient_from_file_path_not_found(self):
|
|
5177
|
-
"""Test Builder class add_ingredient_from_file_path method."""
|
|
5178
|
-
|
|
5179
|
-
# Suppress the specific deprecation warning for this test, as this is a legacy method
|
|
5180
|
-
with warnings.catch_warnings():
|
|
5181
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
|
5182
|
-
|
|
5183
|
-
builder = Builder.from_json(self.manifestDefinition)
|
|
5184
|
-
|
|
5185
|
-
# Test adding ingredient from file path
|
|
5186
|
-
ingredient_json = '{"test": "ingredient_from_file_path"}'
|
|
5187
|
-
|
|
5188
|
-
with self.assertRaises(Error.FileNotFound):
|
|
5189
|
-
builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", "this-file-path-does-not-exist")
|
|
5190
|
-
|
|
5191
|
-
def test_sign_file_using_callback_signer_overloads(self):
|
|
5192
|
-
"""Test signing a file using the sign_file function with a Signer object."""
|
|
5193
|
-
# Create a temporary directory for the test
|
|
5194
|
-
temp_dir = tempfile.mkdtemp()
|
|
5195
|
-
|
|
5196
|
-
try:
|
|
5197
|
-
# Create a temporary output file path
|
|
5198
|
-
output_path = os.path.join(temp_dir, "signed_output_callback.jpg")
|
|
5199
|
-
|
|
5200
|
-
# Create signer with callback
|
|
5201
|
-
signer = Signer.from_callback(
|
|
5202
|
-
callback=self.callback_signer_es256,
|
|
5203
|
-
alg=SigningAlg.ES256,
|
|
5204
|
-
certs=self.certs.decode('utf-8'),
|
|
5205
|
-
tsa_url="http://timestamp.digicert.com"
|
|
5206
|
-
)
|
|
5207
|
-
|
|
5208
|
-
# Overload that returns a JSON string
|
|
5209
|
-
result_json = sign_file(
|
|
5210
|
-
self.testPath,
|
|
5211
|
-
output_path,
|
|
5212
|
-
self.manifestDefinition,
|
|
5213
|
-
signer,
|
|
5214
|
-
False
|
|
5215
|
-
)
|
|
5216
|
-
|
|
5217
|
-
# Verify the output file was created
|
|
5218
|
-
self.assertTrue(os.path.exists(output_path))
|
|
5219
|
-
|
|
5220
|
-
# Verify the result is JSON
|
|
5221
|
-
self.assertIsInstance(result_json, str)
|
|
5222
|
-
self.assertGreater(len(result_json), 0)
|
|
5223
|
-
|
|
5224
|
-
manifest_data = json.loads(result_json)
|
|
5225
|
-
self.assertIn("manifests", manifest_data)
|
|
5226
|
-
self.assertIn("active_manifest", manifest_data)
|
|
5227
|
-
|
|
5228
|
-
output_path_bytes = os.path.join(temp_dir, "signed_output_callback_bytes.jpg")
|
|
5229
|
-
# Overload that returns bytes
|
|
5230
|
-
result_bytes = sign_file(
|
|
5231
|
-
self.testPath,
|
|
5232
|
-
output_path_bytes,
|
|
5233
|
-
self.manifestDefinition,
|
|
5234
|
-
signer,
|
|
5235
|
-
True
|
|
5236
|
-
)
|
|
5237
|
-
|
|
5238
|
-
# Verify the output file was created
|
|
5239
|
-
self.assertTrue(os.path.exists(output_path_bytes))
|
|
5240
|
-
|
|
5241
|
-
# Verify the result is bytes
|
|
5242
|
-
self.assertIsInstance(result_bytes, bytes)
|
|
5243
|
-
self.assertGreater(len(result_bytes), 0)
|
|
5244
|
-
|
|
5245
|
-
# Read the signed file and verify the manifest contains expected content
|
|
5246
|
-
with open(output_path, "rb") as file:
|
|
5247
|
-
reader = Reader("image/jpeg", file)
|
|
5248
|
-
file_manifest_json = reader.json()
|
|
5249
|
-
self.assertIn("Python Test", file_manifest_json)
|
|
5250
|
-
|
|
5251
|
-
finally:
|
|
5252
|
-
shutil.rmtree(temp_dir)
|
|
5253
|
-
|
|
5254
|
-
def test_sign_file_overloads(self):
|
|
5255
|
-
"""Test that the overloaded sign_file function works with both parameter types."""
|
|
5256
|
-
# Create a temporary directory for the test
|
|
5257
|
-
temp_dir = tempfile.mkdtemp()
|
|
5258
|
-
try:
|
|
5259
|
-
# Test with C2paSignerInfo
|
|
5260
|
-
output_path_1 = os.path.join(temp_dir, "signed_output_1.jpg")
|
|
5261
|
-
|
|
5262
|
-
# Load test certificates and key
|
|
5263
|
-
with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
|
|
5264
|
-
certs = cert_file.read()
|
|
5265
|
-
with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
|
|
5266
|
-
key = key_file.read()
|
|
5267
|
-
|
|
5268
|
-
# Create signer info
|
|
5269
|
-
signer_info = C2paSignerInfo(
|
|
5270
|
-
alg=b"es256",
|
|
5271
|
-
sign_cert=certs,
|
|
5272
|
-
private_key=key,
|
|
5273
|
-
ta_url=b"http://timestamp.digicert.com"
|
|
5274
|
-
)
|
|
5275
|
-
|
|
5276
|
-
# Test with C2paSignerInfo parameter - JSON return
|
|
5277
|
-
result_1 = sign_file(
|
|
5278
|
-
self.testPath,
|
|
5279
|
-
output_path_1,
|
|
5280
|
-
self.manifestDefinition,
|
|
5281
|
-
signer_info,
|
|
5282
|
-
False
|
|
5283
|
-
)
|
|
5284
|
-
|
|
5285
|
-
self.assertIsInstance(result_1, str)
|
|
5286
|
-
self.assertTrue(os.path.exists(output_path_1))
|
|
5287
|
-
|
|
5288
|
-
# Test with C2paSignerInfo parameter - bytes return
|
|
5289
|
-
output_path_1_bytes = os.path.join(temp_dir, "signed_output_1_bytes.jpg")
|
|
5290
|
-
result_1_bytes = sign_file(
|
|
5291
|
-
self.testPath,
|
|
5292
|
-
output_path_1_bytes,
|
|
5293
|
-
self.manifestDefinition,
|
|
5294
|
-
signer_info,
|
|
5295
|
-
True
|
|
5296
|
-
)
|
|
5297
|
-
|
|
5298
|
-
self.assertIsInstance(result_1_bytes, bytes)
|
|
5299
|
-
self.assertTrue(os.path.exists(output_path_1_bytes))
|
|
5300
|
-
|
|
5301
|
-
# Test with Signer object
|
|
5302
|
-
output_path_2 = os.path.join(temp_dir, "signed_output_2.jpg")
|
|
5303
|
-
|
|
5304
|
-
# Create a signer from the signer info
|
|
5305
|
-
signer = Signer.from_info(signer_info)
|
|
5306
|
-
|
|
5307
|
-
# Test with Signer parameter - JSON return
|
|
5308
|
-
result_2 = sign_file(
|
|
5309
|
-
self.testPath,
|
|
5310
|
-
output_path_2,
|
|
5311
|
-
self.manifestDefinition,
|
|
5312
|
-
signer,
|
|
5313
|
-
False
|
|
5314
|
-
)
|
|
5315
|
-
|
|
5316
|
-
self.assertIsInstance(result_2, str)
|
|
5317
|
-
self.assertTrue(os.path.exists(output_path_2))
|
|
5318
|
-
|
|
5319
|
-
# Test with Signer parameter - bytes return
|
|
5320
|
-
output_path_2_bytes = os.path.join(temp_dir, "signed_output_2_bytes.jpg")
|
|
5321
|
-
result_2_bytes = sign_file(
|
|
5322
|
-
self.testPath,
|
|
5323
|
-
output_path_2_bytes,
|
|
5324
|
-
self.manifestDefinition,
|
|
5325
|
-
signer,
|
|
5326
|
-
True
|
|
5327
|
-
)
|
|
5328
|
-
|
|
5329
|
-
self.assertIsInstance(result_2_bytes, bytes)
|
|
5330
|
-
self.assertTrue(os.path.exists(output_path_2_bytes))
|
|
5331
|
-
|
|
5332
|
-
# Both JSON results should be similar (same manifest structure)
|
|
5333
|
-
manifest_1 = json.loads(result_1)
|
|
5334
|
-
manifest_2 = json.loads(result_2)
|
|
5335
|
-
|
|
5336
|
-
self.assertIn("manifests", manifest_1)
|
|
5337
|
-
self.assertIn("manifests", manifest_2)
|
|
5338
|
-
self.assertIn("active_manifest", manifest_1)
|
|
5339
|
-
self.assertIn("active_manifest", manifest_2)
|
|
5340
|
-
|
|
5341
|
-
# Both bytes results should be non-empty
|
|
5342
|
-
self.assertGreater(len(result_1_bytes), 0)
|
|
5343
|
-
self.assertGreater(len(result_2_bytes), 0)
|
|
5344
|
-
|
|
5345
|
-
finally:
|
|
5346
|
-
# Clean up the temporary directory
|
|
5347
|
-
shutil.rmtree(temp_dir)
|
|
5348
|
-
|
|
5349
4863
|
def test_sign_file_callback_signer_reports_error(self):
|
|
5350
4864
|
"""Test signing a file using the sign_file method with a callback that reports an error."""
|
|
5351
4865
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# specific language governing permissions and limitations under
|
|
12
12
|
# each license.
|
|
13
13
|
|
|
14
|
+
import ctypes
|
|
14
15
|
import os
|
|
15
16
|
import io
|
|
16
17
|
import json
|
|
@@ -20,13 +21,172 @@ import concurrent.futures
|
|
|
20
21
|
import time
|
|
21
22
|
import asyncio
|
|
22
23
|
import random
|
|
24
|
+
from unittest.mock import MagicMock, patch
|
|
23
25
|
|
|
24
26
|
from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version # noqa: E501
|
|
25
27
|
from c2pa import Context, Settings
|
|
26
|
-
from c2pa.c2pa import Stream
|
|
28
|
+
from c2pa.c2pa import ManagedResource, Stream, LifecycleState
|
|
29
|
+
from c2pa.lib import is_foreign_process, record_owner_pid
|
|
27
30
|
|
|
28
31
|
PROJECT_PATH = os.getcwd()
|
|
29
32
|
FIXTURES_FOLDER = os.path.join(os.path.dirname(__file__), "fixtures")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _ConcreteResource(ManagedResource):
|
|
36
|
+
"""Minimal concrete subclass for testing ManagedResource cleanup."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _make_resource(pid_offset):
|
|
40
|
+
"""Construct a ManagedResource-like object without triggering native init.
|
|
41
|
+
|
|
42
|
+
pid_offset=1 → simulates a forked child (foreign PID)
|
|
43
|
+
pid_offset=0 → same process (normal cleanup)
|
|
44
|
+
pid_offset=None → no _owner_pid stamp (backward-compat: no protection)
|
|
45
|
+
"""
|
|
46
|
+
obj = object.__new__(_ConcreteResource)
|
|
47
|
+
obj._lifecycle_state = LifecycleState.ACTIVE
|
|
48
|
+
obj._handle = ctypes.c_void_p(1) # non-None, non-zero sentinel
|
|
49
|
+
if pid_offset is not None:
|
|
50
|
+
obj._owner_pid = os.getpid() + pid_offset
|
|
51
|
+
return obj
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _make_stream(pid_offset):
|
|
55
|
+
"""Construct a Stream-like object without triggering native init."""
|
|
56
|
+
obj = object.__new__(Stream)
|
|
57
|
+
obj._closed = False
|
|
58
|
+
obj._initialized = True
|
|
59
|
+
obj._stream = MagicMock() # non-None stream handle
|
|
60
|
+
if pid_offset is not None:
|
|
61
|
+
obj._owner_pid = os.getpid() + pid_offset
|
|
62
|
+
return obj
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestManagedResourceForkGuard(unittest.TestCase):
|
|
66
|
+
"""Fork-safety unit tests for ManagedResource and Stream.
|
|
67
|
+
|
|
68
|
+
Verifies that the is_foreign_process() PID guard prevents native frees
|
|
69
|
+
from running in a forked child process (where native mutexes may be held
|
|
70
|
+
by threads that no longer exist, causing deadlock before exec()).
|
|
71
|
+
|
|
72
|
+
No real fork or auth credentials are required; PID mismatch is simulated
|
|
73
|
+
by setting _owner_pid = os.getpid() + 1.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def test_foreign_pid_skips_free(self):
|
|
77
|
+
"""In a forked child (pid_offset=1), no native free should run."""
|
|
78
|
+
obj = _make_resource(pid_offset=1)
|
|
79
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
80
|
+
obj._cleanup_resources()
|
|
81
|
+
mock_lib.c2pa_free.assert_not_called()
|
|
82
|
+
|
|
83
|
+
def test_own_pid_calls_free(self):
|
|
84
|
+
"""In the owning process, cleanup must call c2pa_free normally."""
|
|
85
|
+
obj = _make_resource(pid_offset=0)
|
|
86
|
+
expected_handle = obj._handle
|
|
87
|
+
with patch('c2pa.c2pa._lib'):
|
|
88
|
+
with patch.object(ManagedResource, '_free_native_ptr') as mock_free:
|
|
89
|
+
obj._cleanup_resources()
|
|
90
|
+
mock_free.assert_called_once_with(expected_handle)
|
|
91
|
+
|
|
92
|
+
def test_no_stamp_calls_free(self):
|
|
93
|
+
"""No _owner_pid (backward-compat) must NOT suppress cleanup."""
|
|
94
|
+
obj = _make_resource(pid_offset=None)
|
|
95
|
+
with patch.object(ManagedResource, '_free_native_ptr') as mock_free:
|
|
96
|
+
obj._cleanup_resources()
|
|
97
|
+
mock_free.assert_called_once()
|
|
98
|
+
|
|
99
|
+
def test_foreign_pid_leaves_state_unchanged(self):
|
|
100
|
+
"""Guard returns early; lifecycle state stays ACTIVE (not CLOSED)."""
|
|
101
|
+
obj = _make_resource(pid_offset=1)
|
|
102
|
+
with patch('c2pa.c2pa._lib'):
|
|
103
|
+
obj._cleanup_resources()
|
|
104
|
+
self.assertEqual(obj._lifecycle_state, LifecycleState.ACTIVE)
|
|
105
|
+
|
|
106
|
+
def test_double_cleanup_is_idempotent(self):
|
|
107
|
+
"""Second call is a no-op after successful first cleanup."""
|
|
108
|
+
obj = _make_resource(pid_offset=0)
|
|
109
|
+
with patch.object(ManagedResource, '_free_native_ptr') as mock_free:
|
|
110
|
+
obj._cleanup_resources()
|
|
111
|
+
obj._cleanup_resources()
|
|
112
|
+
mock_free.assert_called_once()
|
|
113
|
+
|
|
114
|
+
def test_foreign_pid_skips_release_via_del(self):
|
|
115
|
+
obj = _make_stream(pid_offset=1)
|
|
116
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
117
|
+
obj.__del__()
|
|
118
|
+
mock_lib.c2pa_release_stream.assert_not_called()
|
|
119
|
+
|
|
120
|
+
def test_own_pid_releases_stream_via_del(self):
|
|
121
|
+
obj = _make_stream(pid_offset=0)
|
|
122
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
123
|
+
obj.__del__()
|
|
124
|
+
mock_lib.c2pa_release_stream.assert_called_once()
|
|
125
|
+
|
|
126
|
+
def test_no_stamp_releases_stream_via_del(self):
|
|
127
|
+
obj = _make_stream(pid_offset=None)
|
|
128
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
129
|
+
obj.__del__()
|
|
130
|
+
mock_lib.c2pa_release_stream.assert_called_once()
|
|
131
|
+
|
|
132
|
+
def test_already_closed_is_noop_via_del(self):
|
|
133
|
+
obj = _make_stream(pid_offset=0)
|
|
134
|
+
obj._closed = True
|
|
135
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
136
|
+
obj.__del__()
|
|
137
|
+
mock_lib.c2pa_release_stream.assert_not_called()
|
|
138
|
+
|
|
139
|
+
def test_foreign_pid_skips_release_via_close(self):
|
|
140
|
+
obj = _make_stream(pid_offset=1)
|
|
141
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
142
|
+
obj.close()
|
|
143
|
+
mock_lib.c2pa_release_stream.assert_not_called()
|
|
144
|
+
self.assertTrue(obj._closed)
|
|
145
|
+
|
|
146
|
+
def test_own_pid_releases_stream_via_close(self):
|
|
147
|
+
obj = _make_stream(pid_offset=0)
|
|
148
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
149
|
+
obj.close()
|
|
150
|
+
mock_lib.c2pa_release_stream.assert_called_once()
|
|
151
|
+
|
|
152
|
+
def test_no_stamp_releases_stream_via_close(self):
|
|
153
|
+
obj = _make_stream(pid_offset=None)
|
|
154
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
155
|
+
obj.close()
|
|
156
|
+
mock_lib.c2pa_release_stream.assert_called_once()
|
|
157
|
+
|
|
158
|
+
def test_already_closed_is_noop_via_close(self):
|
|
159
|
+
obj = _make_stream(pid_offset=0)
|
|
160
|
+
obj._closed = True
|
|
161
|
+
with patch('c2pa.c2pa._lib') as mock_lib:
|
|
162
|
+
obj.close()
|
|
163
|
+
mock_lib.c2pa_release_stream.assert_not_called()
|
|
164
|
+
|
|
165
|
+
def test_foreign_pid_close_marks_closed(self):
|
|
166
|
+
"""close() in forked child must set _closed=True to prevent re-entry,
|
|
167
|
+
and _initialized=False so the public properties report a closed stream."""
|
|
168
|
+
obj = _make_stream(pid_offset=1)
|
|
169
|
+
with patch('c2pa.c2pa._lib'):
|
|
170
|
+
obj.close()
|
|
171
|
+
self.assertTrue(obj._closed)
|
|
172
|
+
self.assertFalse(obj._initialized)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestHelpers(unittest.TestCase):
|
|
176
|
+
|
|
177
|
+
def test_record_and_detect_own_pid(self):
|
|
178
|
+
obj = MagicMock()
|
|
179
|
+
record_owner_pid(obj)
|
|
180
|
+
self.assertFalse(is_foreign_process(obj))
|
|
181
|
+
|
|
182
|
+
def test_detect_foreign_pid(self):
|
|
183
|
+
obj = MagicMock()
|
|
184
|
+
obj._owner_pid = os.getpid() + 1
|
|
185
|
+
self.assertTrue(is_foreign_process(obj))
|
|
186
|
+
|
|
187
|
+
def test_no_stamp_not_foreign(self):
|
|
188
|
+
obj = MagicMock(spec=[]) # no _owner_pid attribute
|
|
189
|
+
self.assertFalse(is_foreign_process(obj))
|
|
30
190
|
DEFAULT_TEST_FILE = os.path.join(FIXTURES_FOLDER, "C.jpg")
|
|
31
191
|
INGREDIENT_TEST_FILE = os.path.join(FIXTURES_FOLDER, "A.jpg")
|
|
32
192
|
ALTERNATIVE_INGREDIENT_TEST_FILE = os.path.join(FIXTURES_FOLDER, "cloud.jpg")
|
|
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
|