c2pa-python 0.17.0__tar.gz → 0.18.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.17.0/src/c2pa_python.egg-info → c2pa_python-0.18.0}/PKG-INFO +1 -1
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/pyproject.toml +1 -1
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/c2pa.py +429 -160
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/lib.py +11 -18
- {c2pa_python-0.17.0 → c2pa_python-0.18.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/tests/test_unit_tests.py +9 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/LICENSE-APACHE +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/LICENSE-MIT +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/MANIFEST.in +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/README.md +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/requirements.txt +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/setup.cfg +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/setup.py +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/build.py +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {c2pa_python-0.17.0 → c2pa_python-0.18.0}/tests/test_unit_tests_threaded.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.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.18.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" }
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import ctypes
|
|
15
15
|
import enum
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
17
18
|
import sys
|
|
18
19
|
import os
|
|
19
20
|
import warnings
|
|
@@ -23,6 +24,10 @@ import io
|
|
|
23
24
|
from .lib import dynamically_load_library
|
|
24
25
|
import mimetypes
|
|
25
26
|
|
|
27
|
+
# Create a module-specific logger
|
|
28
|
+
logger = logging.getLogger("c2pa")
|
|
29
|
+
logger.addHandler(logging.NullHandler())
|
|
30
|
+
|
|
26
31
|
# Define required function names
|
|
27
32
|
_REQUIRED_FUNCTIONS = [
|
|
28
33
|
'c2pa_version',
|
|
@@ -255,6 +260,12 @@ class C2paSignerInfo(ctypes.Structure):
|
|
|
255
260
|
private_key: The private key as a string
|
|
256
261
|
ta_url: The timestamp authority URL as bytes
|
|
257
262
|
"""
|
|
263
|
+
|
|
264
|
+
if sign_cert is None:
|
|
265
|
+
raise ValueError("sign_cert must be set")
|
|
266
|
+
if private_key is None:
|
|
267
|
+
raise ValueError("private_key must be set")
|
|
268
|
+
|
|
258
269
|
# Handle alg parameter: can be C2paSigningAlg enum
|
|
259
270
|
# or string (or bytes), convert as needed
|
|
260
271
|
if isinstance(alg, C2paSigningAlg):
|
|
@@ -545,19 +556,31 @@ def _convert_to_py_string(value) -> str:
|
|
|
545
556
|
return ""
|
|
546
557
|
|
|
547
558
|
py_string = ""
|
|
548
|
-
ptr = ctypes.cast(value, ctypes.c_char_p)
|
|
549
559
|
|
|
550
|
-
#
|
|
551
|
-
if
|
|
552
|
-
|
|
553
|
-
py_string = ptr.value.decode('utf-8', errors='replace')
|
|
554
|
-
except Exception:
|
|
555
|
-
py_string = ""
|
|
560
|
+
# Validate pointer before casting and freeing
|
|
561
|
+
if not isinstance(value, (int, ctypes.c_void_p)) or value == 0:
|
|
562
|
+
return ""
|
|
556
563
|
|
|
557
|
-
|
|
558
|
-
|
|
564
|
+
try:
|
|
565
|
+
ptr = ctypes.cast(value, ctypes.c_char_p)
|
|
566
|
+
|
|
567
|
+
# Only if we got a valid pointer with valid content
|
|
568
|
+
if ptr and ptr.value is not None:
|
|
569
|
+
try:
|
|
570
|
+
py_string = ptr.value.decode('utf-8', errors='replace')
|
|
571
|
+
except Exception:
|
|
572
|
+
py_string = ""
|
|
573
|
+
finally:
|
|
574
|
+
# Only free if we have a valid pointer
|
|
575
|
+
try:
|
|
576
|
+
_lib.c2pa_string_free(value)
|
|
577
|
+
except Exception:
|
|
578
|
+
# Ignore clean up issues
|
|
579
|
+
pass
|
|
580
|
+
except (ctypes.ArgumentError, TypeError, ValueError):
|
|
581
|
+
# Invalid pointer type or value
|
|
582
|
+
return ""
|
|
559
583
|
|
|
560
|
-
# In case of invalid pointer, no free (avoids double-free)
|
|
561
584
|
return py_string
|
|
562
585
|
|
|
563
586
|
|
|
@@ -617,7 +640,7 @@ def sdk_version() -> str:
|
|
|
617
640
|
Returns the underlying c2pa-rs/c2pa-c-ffi version string
|
|
618
641
|
"""
|
|
619
642
|
vstr = version()
|
|
620
|
-
# Example: "c2pa-c/0.
|
|
643
|
+
# Example: "c2pa-c/0.60.1 c2pa-rs/0.60.1"
|
|
621
644
|
for part in vstr.split():
|
|
622
645
|
if part.startswith("c2pa-rs/"):
|
|
623
646
|
return part.split("/", 1)[1]
|
|
@@ -830,7 +853,9 @@ def sign_file(
|
|
|
830
853
|
Use :meth:`Builder.sign` instead.
|
|
831
854
|
|
|
832
855
|
Args:
|
|
833
|
-
source_path: Path to the source file
|
|
856
|
+
source_path: Path to the source file. We will attempt
|
|
857
|
+
to guess the mimetype of the source file based on
|
|
858
|
+
the extension.
|
|
834
859
|
dest_path: Path to write the signed file to
|
|
835
860
|
manifest: The manifest JSON string
|
|
836
861
|
signer_or_info: Either a signer configuration or a signer object
|
|
@@ -887,6 +912,7 @@ def sign_file(
|
|
|
887
912
|
try:
|
|
888
913
|
os.remove(dest_path)
|
|
889
914
|
except OSError:
|
|
915
|
+
logger.warning("Failed to remove destination file")
|
|
890
916
|
pass # Ignore cleanup errors
|
|
891
917
|
|
|
892
918
|
# Re-raise the error
|
|
@@ -1124,9 +1150,30 @@ class Stream:
|
|
|
1124
1150
|
self.close()
|
|
1125
1151
|
|
|
1126
1152
|
def __del__(self):
|
|
1127
|
-
"""Ensure resources are cleaned up if close()
|
|
1128
|
-
|
|
1129
|
-
|
|
1153
|
+
"""Ensure resources are cleaned up if close()wasn't called.
|
|
1154
|
+
|
|
1155
|
+
This destructor only cleans up if the object
|
|
1156
|
+
hasn't been explicitly closed.
|
|
1157
|
+
"""
|
|
1158
|
+
try:
|
|
1159
|
+
# Only cleanup if not already closed and we have a valid stream
|
|
1160
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
1161
|
+
if hasattr(self, '_stream') and self._stream:
|
|
1162
|
+
# Use internal cleanup to avoid calling close() which could
|
|
1163
|
+
# cause issues
|
|
1164
|
+
try:
|
|
1165
|
+
_lib.c2pa_release_stream(self._stream)
|
|
1166
|
+
except Exception:
|
|
1167
|
+
# Destructors shouldn't raise exceptions
|
|
1168
|
+
logger.error("Failed to release Stream")
|
|
1169
|
+
pass
|
|
1170
|
+
finally:
|
|
1171
|
+
self._stream = None
|
|
1172
|
+
self._closed = True
|
|
1173
|
+
self._initialized = False
|
|
1174
|
+
except Exception:
|
|
1175
|
+
# Destructors must not raise exceptions
|
|
1176
|
+
pass
|
|
1130
1177
|
|
|
1131
1178
|
def close(self):
|
|
1132
1179
|
"""Release the stream resources.
|
|
@@ -1148,9 +1195,9 @@ class Stream:
|
|
|
1148
1195
|
try:
|
|
1149
1196
|
_lib.c2pa_release_stream(self._stream)
|
|
1150
1197
|
except Exception as e:
|
|
1151
|
-
|
|
1198
|
+
logger.error(
|
|
1152
1199
|
Stream._ERROR_MESSAGES['stream_error'].format(
|
|
1153
|
-
str(e))
|
|
1200
|
+
str(e)))
|
|
1154
1201
|
finally:
|
|
1155
1202
|
self._stream = None
|
|
1156
1203
|
|
|
@@ -1160,14 +1207,14 @@ class Stream:
|
|
|
1160
1207
|
try:
|
|
1161
1208
|
setattr(self, attr, None)
|
|
1162
1209
|
except Exception as e:
|
|
1163
|
-
|
|
1210
|
+
logger.error(
|
|
1164
1211
|
Stream._ERROR_MESSAGES['callback_error'].format(
|
|
1165
|
-
attr, str(e))
|
|
1212
|
+
attr, str(e)))
|
|
1166
1213
|
|
|
1167
1214
|
except Exception as e:
|
|
1168
|
-
|
|
1215
|
+
logger.error(
|
|
1169
1216
|
Stream._ERROR_MESSAGES['cleanup_error'].format(
|
|
1170
|
-
str(e))
|
|
1217
|
+
str(e)))
|
|
1171
1218
|
finally:
|
|
1172
1219
|
self._closed = True
|
|
1173
1220
|
self._initialized = False
|
|
@@ -1211,26 +1258,71 @@ class Reader:
|
|
|
1211
1258
|
'stream_error': "Error cleaning up stream: {}",
|
|
1212
1259
|
'file_error': "Error cleaning up file: {}",
|
|
1213
1260
|
'reader_cleanup_error': "Error cleaning up reader: {}",
|
|
1214
|
-
'encoding_error': "Invalid UTF-8 characters in input: {}"
|
|
1261
|
+
'encoding_error': "Invalid UTF-8 characters in input: {}",
|
|
1262
|
+
'closed_error': "Reader is closed"
|
|
1215
1263
|
}
|
|
1216
1264
|
|
|
1217
1265
|
@classmethod
|
|
1218
1266
|
def get_supported_mime_types(cls) -> list[str]:
|
|
1267
|
+
"""Get the list of supported MIME types for the Reader.
|
|
1268
|
+
This method retrieves supported MIME types from the native library.
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
List of supported MIME type strings
|
|
1272
|
+
|
|
1273
|
+
Raises:
|
|
1274
|
+
C2paError: If there was an error retrieving the MIME types
|
|
1275
|
+
"""
|
|
1219
1276
|
if cls._supported_mime_types_cache is not None:
|
|
1220
1277
|
return cls._supported_mime_types_cache
|
|
1221
1278
|
|
|
1222
1279
|
count = ctypes.c_size_t()
|
|
1223
1280
|
arr = _lib.c2pa_reader_supported_mime_types(ctypes.byref(count))
|
|
1224
1281
|
|
|
1282
|
+
# Validate the returned array pointer
|
|
1283
|
+
if not arr:
|
|
1284
|
+
# If no array returned, check for errors
|
|
1285
|
+
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
1286
|
+
if error:
|
|
1287
|
+
raise C2paError(f"Failed to get supported MIME types: {error}")
|
|
1288
|
+
# Return empty list if no error but no array
|
|
1289
|
+
return []
|
|
1290
|
+
|
|
1291
|
+
# Validate count value
|
|
1292
|
+
if count.value <= 0:
|
|
1293
|
+
# Free the array even if count is invalid
|
|
1294
|
+
try:
|
|
1295
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
1296
|
+
except Exception:
|
|
1297
|
+
pass
|
|
1298
|
+
return []
|
|
1299
|
+
|
|
1225
1300
|
try:
|
|
1226
|
-
|
|
1227
|
-
|
|
1301
|
+
result = []
|
|
1302
|
+
for i in range(count.value):
|
|
1303
|
+
try:
|
|
1304
|
+
# Validate each array element before accessing
|
|
1305
|
+
if arr[i] is None:
|
|
1306
|
+
continue
|
|
1307
|
+
|
|
1308
|
+
mime_type = arr[i].decode("utf-8", errors='replace')
|
|
1309
|
+
if mime_type:
|
|
1310
|
+
result.append(mime_type)
|
|
1311
|
+
except Exception:
|
|
1312
|
+
# Ignore cleanup errors
|
|
1313
|
+
continue
|
|
1228
1314
|
finally:
|
|
1229
|
-
#
|
|
1230
|
-
|
|
1231
|
-
|
|
1315
|
+
# Always free the native memory, even if string extraction fails
|
|
1316
|
+
try:
|
|
1317
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
1318
|
+
except Exception:
|
|
1319
|
+
# Ignore cleanup errors
|
|
1320
|
+
pass
|
|
1321
|
+
|
|
1322
|
+
# Cache the result
|
|
1323
|
+
if result:
|
|
1324
|
+
cls._supported_mime_types_cache = result
|
|
1232
1325
|
|
|
1233
|
-
cls._supported_mime_types_cache = result
|
|
1234
1326
|
return cls._supported_mime_types_cache
|
|
1235
1327
|
|
|
1236
1328
|
def __init__(self,
|
|
@@ -1241,11 +1333,7 @@ class Reader:
|
|
|
1241
1333
|
"""Create a new Reader.
|
|
1242
1334
|
|
|
1243
1335
|
Args:
|
|
1244
|
-
format_or_path: The format or path to read from
|
|
1245
|
-
The stream API (params format and an open stream) is
|
|
1246
|
-
the recommended way to use the Reader. For paths, we
|
|
1247
|
-
will attempt to guess the mimetype of the source
|
|
1248
|
-
file based on the extension.
|
|
1336
|
+
format_or_path: The format or path to read from
|
|
1249
1337
|
stream: Optional stream to read from (Python stream-like object)
|
|
1250
1338
|
manifest_data: Optional manifest data in bytes
|
|
1251
1339
|
|
|
@@ -1255,22 +1343,31 @@ class Reader:
|
|
|
1255
1343
|
contain invalid UTF-8 characters
|
|
1256
1344
|
"""
|
|
1257
1345
|
|
|
1346
|
+
self._closed = False
|
|
1347
|
+
|
|
1258
1348
|
self._reader = None
|
|
1259
1349
|
self._own_stream = None
|
|
1260
1350
|
|
|
1351
|
+
# This is used to keep track of a file
|
|
1352
|
+
# we may have opened ourselves, and that we need to close later
|
|
1353
|
+
self._backing_file = None
|
|
1354
|
+
|
|
1261
1355
|
if stream is None:
|
|
1262
1356
|
# If we don't get a stream as param:
|
|
1263
1357
|
# Create a stream from the file path in format_or_path
|
|
1264
1358
|
path = str(format_or_path)
|
|
1265
|
-
|
|
1266
1359
|
mime_type = _get_mime_type_from_path(path)
|
|
1267
1360
|
|
|
1361
|
+
if not mime_type:
|
|
1362
|
+
raise C2paError.NotSupported(
|
|
1363
|
+
f"Could not determine MIME type for file: {path}")
|
|
1364
|
+
|
|
1268
1365
|
if mime_type not in Reader.get_supported_mime_types():
|
|
1269
1366
|
raise C2paError.NotSupported(
|
|
1270
1367
|
f"Reader does not support {mime_type}")
|
|
1271
1368
|
|
|
1272
1369
|
try:
|
|
1273
|
-
|
|
1370
|
+
mime_type_str = mime_type.encode('utf-8')
|
|
1274
1371
|
except UnicodeError as e:
|
|
1275
1372
|
raise C2paError.Encoding(
|
|
1276
1373
|
Reader._ERROR_MESSAGES['encoding_error'].format(
|
|
@@ -1282,7 +1379,7 @@ class Reader:
|
|
|
1282
1379
|
self._own_stream = Stream(file)
|
|
1283
1380
|
|
|
1284
1381
|
self._reader = _lib.c2pa_reader_from_stream(
|
|
1285
|
-
|
|
1382
|
+
mime_type_str,
|
|
1286
1383
|
self._own_stream._stream
|
|
1287
1384
|
)
|
|
1288
1385
|
|
|
@@ -1300,13 +1397,13 @@ class Reader:
|
|
|
1300
1397
|
)
|
|
1301
1398
|
|
|
1302
1399
|
# Store the file to close it later
|
|
1303
|
-
self.
|
|
1400
|
+
self._backing_file = file
|
|
1304
1401
|
|
|
1305
1402
|
except Exception as e:
|
|
1306
1403
|
if self._own_stream:
|
|
1307
1404
|
self._own_stream.close()
|
|
1308
|
-
if hasattr(self, '
|
|
1309
|
-
self.
|
|
1405
|
+
if hasattr(self, '_backing_file') and self._backing_file:
|
|
1406
|
+
self._backing_file.close()
|
|
1310
1407
|
raise C2paError.Io(
|
|
1311
1408
|
Reader._ERROR_MESSAGES['io_error'].format(
|
|
1312
1409
|
str(e)))
|
|
@@ -1315,7 +1412,7 @@ class Reader:
|
|
|
1315
1412
|
# If stream is a string, treat it as a path and try to open it
|
|
1316
1413
|
|
|
1317
1414
|
# format_or_path is a format
|
|
1318
|
-
if format_or_path not in Reader.get_supported_mime_types():
|
|
1415
|
+
if format_or_path.lower() not in Reader.get_supported_mime_types():
|
|
1319
1416
|
raise C2paError.NotSupported(
|
|
1320
1417
|
f"Reader does not support {format_or_path}")
|
|
1321
1418
|
|
|
@@ -1324,11 +1421,11 @@ class Reader:
|
|
|
1324
1421
|
self._own_stream = Stream(file)
|
|
1325
1422
|
|
|
1326
1423
|
format_str = str(format_or_path)
|
|
1327
|
-
|
|
1424
|
+
format_bytes = format_str.encode('utf-8')
|
|
1328
1425
|
|
|
1329
1426
|
if manifest_data is None:
|
|
1330
1427
|
self._reader = _lib.c2pa_reader_from_stream(
|
|
1331
|
-
|
|
1428
|
+
format_bytes, self._own_stream._stream)
|
|
1332
1429
|
else:
|
|
1333
1430
|
if not isinstance(manifest_data, bytes):
|
|
1334
1431
|
raise TypeError(
|
|
@@ -1340,7 +1437,7 @@ class Reader:
|
|
|
1340
1437
|
manifest_data)
|
|
1341
1438
|
self._reader = (
|
|
1342
1439
|
_lib.c2pa_reader_from_manifest_data_and_stream(
|
|
1343
|
-
|
|
1440
|
+
format_bytes,
|
|
1344
1441
|
self._own_stream._stream,
|
|
1345
1442
|
manifest_array,
|
|
1346
1443
|
len(manifest_data),
|
|
@@ -1360,19 +1457,19 @@ class Reader:
|
|
|
1360
1457
|
)
|
|
1361
1458
|
)
|
|
1362
1459
|
|
|
1363
|
-
self.
|
|
1460
|
+
self._backing_file = file
|
|
1364
1461
|
except Exception as e:
|
|
1365
1462
|
if self._own_stream:
|
|
1366
1463
|
self._own_stream.close()
|
|
1367
|
-
if hasattr(self, '
|
|
1368
|
-
self.
|
|
1464
|
+
if hasattr(self, '_backing_file') and self._backing_file:
|
|
1465
|
+
self._backing_file.close()
|
|
1369
1466
|
raise C2paError.Io(
|
|
1370
1467
|
Reader._ERROR_MESSAGES['io_error'].format(
|
|
1371
1468
|
str(e)))
|
|
1372
1469
|
else:
|
|
1373
1470
|
# format_or_path is a format string
|
|
1374
1471
|
format_str = str(format_or_path)
|
|
1375
|
-
if format_str not in Reader.get_supported_mime_types():
|
|
1472
|
+
if format_str.lower() not in Reader.get_supported_mime_types():
|
|
1376
1473
|
raise C2paError.NotSupported(
|
|
1377
1474
|
f"Reader does not support {format_str}")
|
|
1378
1475
|
|
|
@@ -1413,11 +1510,85 @@ class Reader:
|
|
|
1413
1510
|
)
|
|
1414
1511
|
|
|
1415
1512
|
def __enter__(self):
|
|
1513
|
+
self._ensure_valid_state()
|
|
1514
|
+
|
|
1515
|
+
if not self._reader:
|
|
1516
|
+
raise C2paError("Invalid Reader when entering context")
|
|
1517
|
+
|
|
1416
1518
|
return self
|
|
1417
1519
|
|
|
1418
1520
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1419
1521
|
self.close()
|
|
1420
1522
|
|
|
1523
|
+
def __del__(self):
|
|
1524
|
+
"""Ensure resources are cleaned up if close() wasn't called.
|
|
1525
|
+
|
|
1526
|
+
This destructor handles cleanup without causing double frees.
|
|
1527
|
+
It only cleans up if the object hasn't been explicitly closed.
|
|
1528
|
+
"""
|
|
1529
|
+
self._cleanup_resources()
|
|
1530
|
+
|
|
1531
|
+
def _ensure_valid_state(self):
|
|
1532
|
+
"""Ensure the reader is in a valid state for operations.
|
|
1533
|
+
|
|
1534
|
+
Raises:
|
|
1535
|
+
C2paError: If the reader is closed or invalid
|
|
1536
|
+
"""
|
|
1537
|
+
if self._closed:
|
|
1538
|
+
raise C2paError(Reader._ERROR_MESSAGES['closed_error'])
|
|
1539
|
+
if not self._reader:
|
|
1540
|
+
raise C2paError(Reader._ERROR_MESSAGES['closed_error'])
|
|
1541
|
+
|
|
1542
|
+
def _cleanup_resources(self):
|
|
1543
|
+
"""Internal cleanup method that releases native resources.
|
|
1544
|
+
|
|
1545
|
+
This method handles the actual cleanup logic and can be called
|
|
1546
|
+
from both close() and __del__ without causing double frees.
|
|
1547
|
+
"""
|
|
1548
|
+
try:
|
|
1549
|
+
# Only cleanup if not already closed and we have a valid reader
|
|
1550
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
1551
|
+
self._closed = True
|
|
1552
|
+
|
|
1553
|
+
# Clean up reader
|
|
1554
|
+
if hasattr(self, '_reader') and self._reader:
|
|
1555
|
+
try:
|
|
1556
|
+
_lib.c2pa_reader_free(self._reader)
|
|
1557
|
+
except Exception:
|
|
1558
|
+
# Cleanup failure doesn't raise exceptions
|
|
1559
|
+
logger.error(
|
|
1560
|
+
"Failed to free native Reader resources"
|
|
1561
|
+
)
|
|
1562
|
+
pass
|
|
1563
|
+
finally:
|
|
1564
|
+
self._reader = None
|
|
1565
|
+
|
|
1566
|
+
# Clean up stream
|
|
1567
|
+
if hasattr(self, '_own_stream') and self._own_stream:
|
|
1568
|
+
try:
|
|
1569
|
+
self._own_stream.close()
|
|
1570
|
+
except Exception:
|
|
1571
|
+
# Cleanup failure doesn't raise exceptions
|
|
1572
|
+
logger.error("Failed to close Reader stream")
|
|
1573
|
+
pass
|
|
1574
|
+
finally:
|
|
1575
|
+
self._own_stream = None
|
|
1576
|
+
|
|
1577
|
+
# Clean up backing file (if needed)
|
|
1578
|
+
if self._backing_file:
|
|
1579
|
+
try:
|
|
1580
|
+
self._backing_file.close()
|
|
1581
|
+
except Exception:
|
|
1582
|
+
# Cleanup failure doesn't raise exceptions
|
|
1583
|
+
logger.warning("Failed to close Reader backing file")
|
|
1584
|
+
pass
|
|
1585
|
+
finally:
|
|
1586
|
+
self._backing_file = None
|
|
1587
|
+
|
|
1588
|
+
except Exception:
|
|
1589
|
+
# Ensure we don't raise exceptions during cleanup
|
|
1590
|
+
pass
|
|
1591
|
+
|
|
1421
1592
|
def close(self):
|
|
1422
1593
|
"""Release the reader resources.
|
|
1423
1594
|
|
|
@@ -1426,55 +1597,17 @@ class Reader:
|
|
|
1426
1597
|
Errors during cleanup are logged but not raised to ensure cleanup.
|
|
1427
1598
|
Multiple calls to close() are handled gracefully.
|
|
1428
1599
|
"""
|
|
1429
|
-
|
|
1430
|
-
# Track if we've already cleaned up
|
|
1431
|
-
if not hasattr(self, '_closed'):
|
|
1432
|
-
self._closed = False
|
|
1433
|
-
|
|
1434
1600
|
if self._closed:
|
|
1435
1601
|
return
|
|
1436
1602
|
|
|
1437
1603
|
try:
|
|
1438
|
-
#
|
|
1439
|
-
|
|
1440
|
-
try:
|
|
1441
|
-
_lib.c2pa_reader_free(self._reader)
|
|
1442
|
-
except Exception as e:
|
|
1443
|
-
print(
|
|
1444
|
-
Reader._ERROR_MESSAGES['reader_cleanup_error'].format(
|
|
1445
|
-
str(e)), file=sys.stderr)
|
|
1446
|
-
finally:
|
|
1447
|
-
self._reader = None
|
|
1448
|
-
|
|
1449
|
-
# Clean up stream
|
|
1450
|
-
if hasattr(self, '_own_stream') and self._own_stream:
|
|
1451
|
-
try:
|
|
1452
|
-
self._own_stream.close()
|
|
1453
|
-
except Exception as e:
|
|
1454
|
-
print(
|
|
1455
|
-
Reader._ERROR_MESSAGES['stream_error'].format(
|
|
1456
|
-
str(e)), file=sys.stderr)
|
|
1457
|
-
finally:
|
|
1458
|
-
self._own_stream = None
|
|
1459
|
-
|
|
1460
|
-
# Clean up file
|
|
1461
|
-
if hasattr(self, '_file_like_stream'):
|
|
1462
|
-
try:
|
|
1463
|
-
self._file_like_stream.close()
|
|
1464
|
-
except Exception as e:
|
|
1465
|
-
print(
|
|
1466
|
-
Reader._ERROR_MESSAGES['file_error'].format(
|
|
1467
|
-
str(e)), file=sys.stderr)
|
|
1468
|
-
finally:
|
|
1469
|
-
self._file_like_stream = None
|
|
1470
|
-
|
|
1471
|
-
# Clear any stored strings
|
|
1472
|
-
if hasattr(self, '_strings'):
|
|
1473
|
-
self._strings.clear()
|
|
1604
|
+
# Use the internal cleanup method
|
|
1605
|
+
self._cleanup_resources()
|
|
1474
1606
|
except Exception as e:
|
|
1475
|
-
|
|
1607
|
+
# Log any unexpected errors during close
|
|
1608
|
+
logger.error(
|
|
1476
1609
|
Reader._ERROR_MESSAGES['cleanup_error'].format(
|
|
1477
|
-
str(e))
|
|
1610
|
+
str(e)))
|
|
1478
1611
|
finally:
|
|
1479
1612
|
self._closed = True
|
|
1480
1613
|
|
|
@@ -1488,8 +1621,8 @@ class Reader:
|
|
|
1488
1621
|
C2paError: If there was an error getting the JSON
|
|
1489
1622
|
"""
|
|
1490
1623
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1624
|
+
self._ensure_valid_state()
|
|
1625
|
+
|
|
1493
1626
|
result = _lib.c2pa_reader_json(self._reader)
|
|
1494
1627
|
|
|
1495
1628
|
if result is None:
|
|
@@ -1513,13 +1646,12 @@ class Reader:
|
|
|
1513
1646
|
Raises:
|
|
1514
1647
|
C2paError: If there was an error writing the resource to stream
|
|
1515
1648
|
"""
|
|
1516
|
-
|
|
1517
|
-
raise C2paError("Reader is closed")
|
|
1649
|
+
self._ensure_valid_state()
|
|
1518
1650
|
|
|
1519
|
-
|
|
1651
|
+
uri_str = uri.encode('utf-8')
|
|
1520
1652
|
with Stream(stream) as stream_obj:
|
|
1521
1653
|
result = _lib.c2pa_reader_resource_to_stream(
|
|
1522
|
-
self._reader,
|
|
1654
|
+
self._reader, uri_str, stream_obj._stream)
|
|
1523
1655
|
|
|
1524
1656
|
if result < 0:
|
|
1525
1657
|
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
@@ -1553,7 +1685,17 @@ class Signer:
|
|
|
1553
1685
|
|
|
1554
1686
|
Note: This constructor is not meant to be called directly.
|
|
1555
1687
|
Use from_info() or from_callback() instead.
|
|
1688
|
+
|
|
1689
|
+
Args:
|
|
1690
|
+
signer_ptr: Pointer to the native C2PA signer
|
|
1691
|
+
|
|
1692
|
+
Raises:
|
|
1693
|
+
C2paError: If the signer pointer is invalid
|
|
1556
1694
|
"""
|
|
1695
|
+
# Validate pointer before assignment
|
|
1696
|
+
if not signer_ptr:
|
|
1697
|
+
raise C2paError("Invalid signer pointer: pointer is null")
|
|
1698
|
+
|
|
1557
1699
|
self._signer = signer_ptr
|
|
1558
1700
|
self._closed = False
|
|
1559
1701
|
|
|
@@ -1679,9 +1821,9 @@ class Signer:
|
|
|
1679
1821
|
# Native code expects the signed len to be returned, we oblige
|
|
1680
1822
|
return actual_len
|
|
1681
1823
|
except Exception as e:
|
|
1682
|
-
|
|
1824
|
+
logger.error(
|
|
1683
1825
|
cls._ERROR_MESSAGES['callback_error'].format(
|
|
1684
|
-
str(e))
|
|
1826
|
+
str(e)))
|
|
1685
1827
|
# Error: exception raised, invalid so return -1,
|
|
1686
1828
|
# native code will handle the error when seeing -1
|
|
1687
1829
|
return -1
|
|
@@ -1728,40 +1870,85 @@ class Signer:
|
|
|
1728
1870
|
|
|
1729
1871
|
def __enter__(self):
|
|
1730
1872
|
"""Context manager entry."""
|
|
1731
|
-
|
|
1732
|
-
|
|
1873
|
+
self._ensure_valid_state()
|
|
1874
|
+
|
|
1875
|
+
if not self._signer:
|
|
1876
|
+
raise C2paError("Invalid signer pointer: pointer is null")
|
|
1877
|
+
|
|
1733
1878
|
return self
|
|
1734
1879
|
|
|
1735
1880
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1736
1881
|
"""Context manager exit."""
|
|
1737
1882
|
self.close()
|
|
1738
1883
|
|
|
1884
|
+
def _cleanup_resources(self):
|
|
1885
|
+
"""Internal cleanup method that releases native resources.
|
|
1886
|
+
|
|
1887
|
+
This method handles the actual cleanup logic and can be called
|
|
1888
|
+
from both close() and __del__ without causing double frees.
|
|
1889
|
+
"""
|
|
1890
|
+
try:
|
|
1891
|
+
if not self._closed and self._signer:
|
|
1892
|
+
self._closed = True
|
|
1893
|
+
|
|
1894
|
+
try:
|
|
1895
|
+
_lib.c2pa_signer_free(self._signer)
|
|
1896
|
+
except Exception:
|
|
1897
|
+
# Log cleanup errors but don't raise exceptions
|
|
1898
|
+
logger.error("Failed to free native Signer resources")
|
|
1899
|
+
finally:
|
|
1900
|
+
self._signer = None
|
|
1901
|
+
|
|
1902
|
+
# Clean up callback reference
|
|
1903
|
+
if self._callback_cb:
|
|
1904
|
+
self._callback_cb = None
|
|
1905
|
+
|
|
1906
|
+
except Exception:
|
|
1907
|
+
# Ensure we don't raise exceptions during cleanup
|
|
1908
|
+
pass
|
|
1909
|
+
|
|
1910
|
+
def _ensure_valid_state(self):
|
|
1911
|
+
"""Ensure the signer is in a valid state for operations.
|
|
1912
|
+
|
|
1913
|
+
Raises:
|
|
1914
|
+
C2paError: If the signer is closed or invalid
|
|
1915
|
+
"""
|
|
1916
|
+
if self._closed:
|
|
1917
|
+
raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
|
|
1918
|
+
if not self._signer:
|
|
1919
|
+
raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
|
|
1920
|
+
|
|
1739
1921
|
def close(self):
|
|
1740
1922
|
"""Release the signer resources.
|
|
1741
1923
|
|
|
1742
1924
|
This method ensures all resources are properly cleaned up,
|
|
1743
1925
|
even if errors occur during cleanup.
|
|
1744
|
-
|
|
1745
|
-
|
|
1926
|
+
|
|
1927
|
+
Note:
|
|
1928
|
+
Multiple calls to close() are handled gracefully.
|
|
1929
|
+
Errors during cleanup are logged but not raised
|
|
1930
|
+
to ensure cleanup.
|
|
1746
1931
|
"""
|
|
1747
1932
|
if self._closed:
|
|
1748
1933
|
return
|
|
1749
1934
|
|
|
1750
1935
|
try:
|
|
1751
|
-
if
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1936
|
+
# Validate pointer before cleanup if it exists
|
|
1937
|
+
if self._signer and self._signer != 0:
|
|
1938
|
+
# Use the internal cleanup method
|
|
1939
|
+
self._cleanup_resources()
|
|
1940
|
+
else:
|
|
1941
|
+
# Make sure to release the callback
|
|
1942
|
+
if self._callback_cb:
|
|
1943
|
+
self._callback_cb = None
|
|
1944
|
+
|
|
1760
1945
|
except Exception as e:
|
|
1761
|
-
|
|
1946
|
+
# Log any unexpected errors during close
|
|
1947
|
+
logger.error(
|
|
1762
1948
|
Signer._ERROR_MESSAGES['cleanup_error'].format(
|
|
1763
|
-
str(e))
|
|
1949
|
+
str(e)))
|
|
1764
1950
|
finally:
|
|
1951
|
+
# Always mark as closed, regardless of cleanup success
|
|
1765
1952
|
self._closed = True
|
|
1766
1953
|
|
|
1767
1954
|
def reserve_size(self) -> int:
|
|
@@ -1773,8 +1960,7 @@ class Signer:
|
|
|
1773
1960
|
Raises:
|
|
1774
1961
|
C2paError: If there was an error getting the size
|
|
1775
1962
|
"""
|
|
1776
|
-
|
|
1777
|
-
raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
|
|
1963
|
+
self._ensure_valid_state()
|
|
1778
1964
|
|
|
1779
1965
|
result = _lib.c2pa_signer_reserve_size(self._signer)
|
|
1780
1966
|
|
|
@@ -1820,22 +2006,65 @@ class Builder:
|
|
|
1820
2006
|
|
|
1821
2007
|
@classmethod
|
|
1822
2008
|
def get_supported_mime_types(cls) -> list[str]:
|
|
2009
|
+
"""Get the list of supported MIME types for the Builder.
|
|
2010
|
+
This method retrieves supported MIME types from the native library.
|
|
2011
|
+
|
|
2012
|
+
Returns:
|
|
2013
|
+
List of supported MIME type strings
|
|
2014
|
+
|
|
2015
|
+
Raises:
|
|
2016
|
+
C2paError: If there was an error retrieving the MIME types
|
|
2017
|
+
"""
|
|
1823
2018
|
if cls._supported_mime_types_cache is not None:
|
|
1824
2019
|
return cls._supported_mime_types_cache
|
|
1825
2020
|
|
|
1826
2021
|
count = ctypes.c_size_t()
|
|
1827
2022
|
arr = _lib.c2pa_builder_supported_mime_types(ctypes.byref(count))
|
|
1828
2023
|
|
|
2024
|
+
# Validate the returned array pointer
|
|
2025
|
+
if not arr:
|
|
2026
|
+
# If no array returned, check for errors
|
|
2027
|
+
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
2028
|
+
if error:
|
|
2029
|
+
raise C2paError(f"Failed to get supported MIME types: {error}")
|
|
2030
|
+
# Return empty list if no error but no array
|
|
2031
|
+
return []
|
|
2032
|
+
|
|
2033
|
+
# Validate count value
|
|
2034
|
+
if count.value <= 0:
|
|
2035
|
+
# Free the array even if count is invalid
|
|
2036
|
+
try:
|
|
2037
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
2038
|
+
except Exception:
|
|
2039
|
+
pass
|
|
2040
|
+
return []
|
|
2041
|
+
|
|
1829
2042
|
try:
|
|
1830
|
-
|
|
1831
|
-
|
|
2043
|
+
result = []
|
|
2044
|
+
for i in range(count.value):
|
|
2045
|
+
try:
|
|
2046
|
+
# Validate each array element before accessing
|
|
2047
|
+
if arr[i] is None:
|
|
2048
|
+
continue
|
|
2049
|
+
|
|
2050
|
+
mime_type = arr[i].decode("utf-8", errors='replace')
|
|
2051
|
+
if mime_type:
|
|
2052
|
+
result.append(mime_type)
|
|
2053
|
+
except Exception:
|
|
2054
|
+
# Ignore decoding failures
|
|
2055
|
+
continue
|
|
1832
2056
|
finally:
|
|
1833
|
-
#
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2057
|
+
# Always free the native memory, even if string extraction fails
|
|
2058
|
+
try:
|
|
2059
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
2060
|
+
except Exception:
|
|
2061
|
+
# Ignore cleanup errors
|
|
2062
|
+
pass
|
|
2063
|
+
|
|
2064
|
+
# Cache the result
|
|
2065
|
+
if result:
|
|
2066
|
+
cls._supported_mime_types_cache = result
|
|
1837
2067
|
|
|
1838
|
-
cls._supported_mime_types_cache = result
|
|
1839
2068
|
return cls._supported_mime_types_cache
|
|
1840
2069
|
|
|
1841
2070
|
def __init__(self, manifest_json: Any):
|
|
@@ -1849,6 +2078,7 @@ class Builder:
|
|
|
1849
2078
|
C2paError.Encoding: If manifest JSON contains invalid UTF-8 chars
|
|
1850
2079
|
C2paError.Json: If the manifest JSON cannot be serialized
|
|
1851
2080
|
"""
|
|
2081
|
+
self._closed = False
|
|
1852
2082
|
self._builder = None
|
|
1853
2083
|
|
|
1854
2084
|
if not isinstance(manifest_json, str):
|
|
@@ -1912,6 +2142,9 @@ class Builder:
|
|
|
1912
2142
|
builder._builder = _lib.c2pa_builder_from_archive(stream_obj._stream)
|
|
1913
2143
|
|
|
1914
2144
|
if not builder._builder:
|
|
2145
|
+
# Clean up the stream object if builder creation fails
|
|
2146
|
+
stream_obj.close()
|
|
2147
|
+
|
|
1915
2148
|
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
1916
2149
|
if error:
|
|
1917
2150
|
raise C2paError(error)
|
|
@@ -1919,10 +2152,48 @@ class Builder:
|
|
|
1919
2152
|
|
|
1920
2153
|
return builder
|
|
1921
2154
|
|
|
2155
|
+
def _ensure_valid_state(self):
|
|
2156
|
+
"""Ensure the builder is in a valid state for operations.
|
|
2157
|
+
|
|
2158
|
+
Raises:
|
|
2159
|
+
C2paError: If the builder is closed or invalid
|
|
2160
|
+
"""
|
|
2161
|
+
if self._closed:
|
|
2162
|
+
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2163
|
+
if not self._builder:
|
|
2164
|
+
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2165
|
+
|
|
2166
|
+
def _cleanup_resources(self):
|
|
2167
|
+
"""Internal cleanup method that releases native resources.
|
|
2168
|
+
|
|
2169
|
+
This method handles the actual cleanup logic and can be called
|
|
2170
|
+
from both close() and __del__ without causing double frees.
|
|
2171
|
+
"""
|
|
2172
|
+
try:
|
|
2173
|
+
# Only cleanup if not already closed and we have a valid builder
|
|
2174
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
2175
|
+
self._closed = True
|
|
2176
|
+
|
|
2177
|
+
if hasattr(
|
|
2178
|
+
self,
|
|
2179
|
+
'_builder') and self._builder and self._builder != 0:
|
|
2180
|
+
try:
|
|
2181
|
+
_lib.c2pa_builder_free(self._builder)
|
|
2182
|
+
except Exception:
|
|
2183
|
+
# Log cleanup errors but don't raise exceptions
|
|
2184
|
+
logger.error(
|
|
2185
|
+
"Failed to release native Builder resources"
|
|
2186
|
+
)
|
|
2187
|
+
pass
|
|
2188
|
+
finally:
|
|
2189
|
+
self._builder = None
|
|
2190
|
+
except Exception:
|
|
2191
|
+
# Ensure we don't raise exceptions during cleanup
|
|
2192
|
+
pass
|
|
2193
|
+
|
|
1922
2194
|
def __del__(self):
|
|
1923
2195
|
"""Ensure resources are cleaned up if close() wasn't called."""
|
|
1924
|
-
|
|
1925
|
-
self.close()
|
|
2196
|
+
self._cleanup_resources()
|
|
1926
2197
|
|
|
1927
2198
|
def close(self):
|
|
1928
2199
|
"""Release the builder resources.
|
|
@@ -1932,32 +2203,26 @@ class Builder:
|
|
|
1932
2203
|
Errors during cleanup are logged but not raised to ensure cleanup.
|
|
1933
2204
|
Multiple calls to close() are handled gracefully.
|
|
1934
2205
|
"""
|
|
1935
|
-
# Track if we've already cleaned up
|
|
1936
|
-
if not hasattr(self, '_closed'):
|
|
1937
|
-
self._closed = False
|
|
1938
|
-
|
|
1939
2206
|
if self._closed:
|
|
1940
2207
|
return
|
|
1941
2208
|
|
|
1942
2209
|
try:
|
|
1943
|
-
#
|
|
1944
|
-
|
|
1945
|
-
try:
|
|
1946
|
-
_lib.c2pa_builder_free(self._builder)
|
|
1947
|
-
except Exception as e:
|
|
1948
|
-
print(
|
|
1949
|
-
Builder._ERROR_MESSAGES['builder_cleanup'].format(
|
|
1950
|
-
str(e)), file=sys.stderr)
|
|
1951
|
-
finally:
|
|
1952
|
-
self._builder = None
|
|
2210
|
+
# Use the internal cleanup method
|
|
2211
|
+
self._cleanup_resources()
|
|
1953
2212
|
except Exception as e:
|
|
1954
|
-
|
|
2213
|
+
# Log any unexpected errors during close
|
|
2214
|
+
logger.error(
|
|
1955
2215
|
Builder._ERROR_MESSAGES['cleanup_error'].format(
|
|
1956
|
-
str(e))
|
|
2216
|
+
str(e)))
|
|
1957
2217
|
finally:
|
|
1958
2218
|
self._closed = True
|
|
1959
2219
|
|
|
1960
2220
|
def __enter__(self):
|
|
2221
|
+
self._ensure_valid_state()
|
|
2222
|
+
|
|
2223
|
+
if not self._builder:
|
|
2224
|
+
raise C2paError("Invalid Builder when entering context")
|
|
2225
|
+
|
|
1961
2226
|
return self
|
|
1962
2227
|
|
|
1963
2228
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
@@ -1970,9 +2235,7 @@ class Builder:
|
|
|
1970
2235
|
into the asset when signing.
|
|
1971
2236
|
This is useful when creating cloud or sidecar manifests.
|
|
1972
2237
|
"""
|
|
1973
|
-
|
|
1974
|
-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
1975
|
-
|
|
2238
|
+
self._ensure_valid_state()
|
|
1976
2239
|
_lib.c2pa_builder_set_no_embed(self._builder)
|
|
1977
2240
|
|
|
1978
2241
|
def set_remote_url(self, remote_url: str):
|
|
@@ -1987,8 +2250,7 @@ class Builder:
|
|
|
1987
2250
|
Raises:
|
|
1988
2251
|
C2paError: If there was an error setting the remote URL
|
|
1989
2252
|
"""
|
|
1990
|
-
|
|
1991
|
-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2253
|
+
self._ensure_valid_state()
|
|
1992
2254
|
|
|
1993
2255
|
url_str = remote_url.encode('utf-8')
|
|
1994
2256
|
result = _lib.c2pa_builder_set_remote_url(self._builder, url_str)
|
|
@@ -2011,8 +2273,7 @@ class Builder:
|
|
|
2011
2273
|
Raises:
|
|
2012
2274
|
C2paError: If there was an error adding the resource
|
|
2013
2275
|
"""
|
|
2014
|
-
|
|
2015
|
-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2276
|
+
self._ensure_valid_state()
|
|
2016
2277
|
|
|
2017
2278
|
uri_str = uri.encode('utf-8')
|
|
2018
2279
|
with Stream(stream) as stream_obj:
|
|
@@ -2194,7 +2455,11 @@ class Builder:
|
|
|
2194
2455
|
if not self._builder:
|
|
2195
2456
|
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2196
2457
|
|
|
2197
|
-
|
|
2458
|
+
# Validate signer pointer before use
|
|
2459
|
+
if not signer or not hasattr(signer, '_signer') or not signer._signer:
|
|
2460
|
+
raise C2paError("Invalid or closed signer")
|
|
2461
|
+
|
|
2462
|
+
if format.lower() not in Builder.get_supported_mime_types():
|
|
2198
2463
|
raise C2paError.NotSupported(
|
|
2199
2464
|
f"Builder does not support {format}")
|
|
2200
2465
|
|
|
@@ -2202,14 +2467,18 @@ class Builder:
|
|
|
2202
2467
|
manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)()
|
|
2203
2468
|
|
|
2204
2469
|
# c2pa_builder_sign uses streams
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2470
|
+
try:
|
|
2471
|
+
result = _lib.c2pa_builder_sign(
|
|
2472
|
+
self._builder,
|
|
2473
|
+
format_str,
|
|
2474
|
+
source_stream._stream,
|
|
2475
|
+
dest_stream._stream,
|
|
2476
|
+
signer._signer,
|
|
2477
|
+
ctypes.byref(manifest_bytes_ptr)
|
|
2478
|
+
)
|
|
2479
|
+
except Exception as e:
|
|
2480
|
+
# Handle errors during the C function call
|
|
2481
|
+
raise C2paError(f"Error calling c2pa_builder_sign: {str(e)}")
|
|
2213
2482
|
|
|
2214
2483
|
if result < 0:
|
|
2215
2484
|
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
@@ -2226,8 +2495,6 @@ class Builder:
|
|
|
2226
2495
|
ctypes.memmove(temp_buffer, manifest_bytes_ptr, result)
|
|
2227
2496
|
manifest_bytes = bytes(temp_buffer)
|
|
2228
2497
|
except Exception:
|
|
2229
|
-
# If there's any error accessing the memory, just return
|
|
2230
|
-
# empty bytes
|
|
2231
2498
|
manifest_bytes = b""
|
|
2232
2499
|
finally:
|
|
2233
2500
|
# Always free the C-allocated memory,
|
|
@@ -2235,7 +2502,9 @@ class Builder:
|
|
|
2235
2502
|
try:
|
|
2236
2503
|
_lib.c2pa_manifest_bytes_free(manifest_bytes_ptr)
|
|
2237
2504
|
except Exception:
|
|
2238
|
-
|
|
2505
|
+
logger.error(
|
|
2506
|
+
"Failed to release native manifest bytes memory"
|
|
2507
|
+
)
|
|
2239
2508
|
pass
|
|
2240
2509
|
|
|
2241
2510
|
return manifest_bytes
|
|
@@ -2308,7 +2577,7 @@ class Builder:
|
|
|
2308
2577
|
Raises:
|
|
2309
2578
|
C2paError: If there was an error during signing
|
|
2310
2579
|
"""
|
|
2311
|
-
|
|
2580
|
+
# Get the MIME type from the file extension
|
|
2312
2581
|
mime_type = _get_mime_type_from_path(source_path)
|
|
2313
2582
|
|
|
2314
2583
|
try:
|
|
@@ -16,8 +16,9 @@ from enum import Enum
|
|
|
16
16
|
# Debug flag for library loading
|
|
17
17
|
DEBUG_LIBRARY_LOADING = False
|
|
18
18
|
|
|
19
|
-
# Create a module-specific logger with NullHandler
|
|
20
|
-
|
|
19
|
+
# Create a module-specific logger with NullHandler
|
|
20
|
+
# to avoid interfering with global configuration
|
|
21
|
+
logger = logging.getLogger("c2pa.loader")
|
|
21
22
|
logger.addHandler(logging.NullHandler())
|
|
22
23
|
|
|
23
24
|
|
|
@@ -107,8 +108,7 @@ def _load_single_library(lib_name: str,
|
|
|
107
108
|
The loaded library or None if loading failed
|
|
108
109
|
"""
|
|
109
110
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
110
|
-
logger.info(
|
|
111
|
-
f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}")
|
|
111
|
+
logger.info(f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}")
|
|
112
112
|
current_arch = _get_architecture()
|
|
113
113
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
114
114
|
logger.info(f"Current architecture: {current_arch}")
|
|
@@ -125,12 +125,10 @@ def _load_single_library(lib_name: str,
|
|
|
125
125
|
except Exception as e:
|
|
126
126
|
error_msg = str(e)
|
|
127
127
|
if "incompatible architecture" in error_msg:
|
|
128
|
-
logger.error(
|
|
129
|
-
f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}")
|
|
128
|
+
logger.error(f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}")
|
|
130
129
|
logger.error(f"Error details: {error_msg}")
|
|
131
130
|
else:
|
|
132
|
-
logger.error(
|
|
133
|
-
f"Failed to load library from {lib_path}: {e}")
|
|
131
|
+
logger.error(f"Failed to load library from {lib_path}: {e}")
|
|
134
132
|
else:
|
|
135
133
|
logger.debug(f"Library not found at: {lib_path}")
|
|
136
134
|
return None
|
|
@@ -216,16 +214,14 @@ def dynamically_load_library(
|
|
|
216
214
|
env_lib_name = os.environ.get("C2PA_LIBRARY_NAME")
|
|
217
215
|
if env_lib_name:
|
|
218
216
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
219
|
-
logger.info(
|
|
220
|
-
f"Using library name from env var C2PA_LIBRARY_NAME: {env_lib_name}")
|
|
217
|
+
logger.info(f"Using library name from env var C2PA_LIBRARY_NAME: {env_lib_name}")
|
|
221
218
|
try:
|
|
222
219
|
possible_paths = _get_possible_search_paths()
|
|
223
220
|
lib = _load_single_library(env_lib_name, possible_paths)
|
|
224
221
|
if lib:
|
|
225
222
|
return lib
|
|
226
223
|
else:
|
|
227
|
-
logger.error(
|
|
228
|
-
f"Could not find library {env_lib_name} in any of the search paths")
|
|
224
|
+
logger.error(f"Could not find library {env_lib_name} in any of the search paths")
|
|
229
225
|
# Continue with normal loading if environment variable library
|
|
230
226
|
# name fails
|
|
231
227
|
except Exception as e:
|
|
@@ -241,12 +237,9 @@ def dynamically_load_library(
|
|
|
241
237
|
if not lib:
|
|
242
238
|
platform_id = get_platform_identifier()
|
|
243
239
|
current_arch = _get_architecture()
|
|
244
|
-
logger.error(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
f"Platform: {platform_id}, Architecture: {current_arch}")
|
|
248
|
-
raise RuntimeError(
|
|
249
|
-
f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})")
|
|
240
|
+
logger.error(f"Could not find {lib_name} in any of the search paths: {[str(p) for p in possible_paths]}")
|
|
241
|
+
logger.error(f"Platform: {platform_id}, Architecture: {current_arch}")
|
|
242
|
+
raise RuntimeError(f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})")
|
|
250
243
|
return lib
|
|
251
244
|
|
|
252
245
|
# Default path (no library name provided in the environment)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.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>
|
|
@@ -150,6 +150,15 @@ class TestReader(unittest.TestCase):
|
|
|
150
150
|
# Just run and verify there is no crash
|
|
151
151
|
json.loads(reader.json())
|
|
152
152
|
|
|
153
|
+
def test_read_dng_upper_case_from_stream(self):
|
|
154
|
+
test_path = os.path.join(self.data_dir, "C.dng")
|
|
155
|
+
with open(test_path, "rb") as file:
|
|
156
|
+
file_content = file.read()
|
|
157
|
+
|
|
158
|
+
with Reader("DNG", io.BytesIO(file_content)) as reader:
|
|
159
|
+
# Just run and verify there is no crash
|
|
160
|
+
json.loads(reader.json())
|
|
161
|
+
|
|
153
162
|
def test_read_dng_file_from_path(self):
|
|
154
163
|
test_path = os.path.join(self.data_dir, "C.dng")
|
|
155
164
|
|
|
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
|