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.
Files changed (22) hide show
  1. {c2pa_python-0.17.0/src/c2pa_python.egg-info → c2pa_python-0.18.0}/PKG-INFO +1 -1
  2. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/pyproject.toml +1 -1
  3. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/c2pa.py +429 -160
  4. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/lib.py +11 -18
  5. {c2pa_python-0.17.0 → c2pa_python-0.18.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  6. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/tests/test_unit_tests.py +9 -0
  7. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/LICENSE-APACHE +0 -0
  8. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/LICENSE-MIT +0 -0
  9. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/MANIFEST.in +0 -0
  10. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/README.md +0 -0
  11. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/requirements.txt +0 -0
  12. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/scripts/download_artifacts.py +0 -0
  13. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/setup.cfg +0 -0
  14. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/setup.py +0 -0
  15. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/__init__.py +0 -0
  16. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa/build.py +0 -0
  17. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.17.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {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.17.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.17.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
- # Only if we got a valid pointer
551
- if ptr and ptr.value is not None:
552
- try:
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
- # Free the Rust-allocated memory
558
- _lib.c2pa_string_free(value)
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.49.5 c2pa-rs/0.49.5"
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() wasn't called."""
1128
- if hasattr(self, '_closed'):
1129
- self.close()
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
- print(
1198
+ logger.error(
1152
1199
  Stream._ERROR_MESSAGES['stream_error'].format(
1153
- str(e)), file=sys.stderr)
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
- print(
1210
+ logger.error(
1164
1211
  Stream._ERROR_MESSAGES['callback_error'].format(
1165
- attr, str(e)), file=sys.stderr)
1212
+ attr, str(e)))
1166
1213
 
1167
1214
  except Exception as e:
1168
- print(
1215
+ logger.error(
1169
1216
  Stream._ERROR_MESSAGES['cleanup_error'].format(
1170
- str(e)), file=sys.stderr)
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
- # CDecode values to place them in Python managed memory
1227
- result = [arr[i].decode("utf-8") for i in range(count.value)]
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
- # Release native memory, as per API contract
1230
- # c2pa_reader_supported_mime_types must call c2pa_free_string_array
1231
- _lib.c2pa_free_string_array(arr, count.value)
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
- self._mime_type_str = mime_type.encode('utf-8')
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
- self._mime_type_str,
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._file_like_stream = file
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, '_file_like_stream'):
1309
- self._file_like_stream.close()
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
- self._format_str = format_str.encode('utf-8')
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
- self._format_str, self._own_stream._stream)
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
- self._format_str,
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._file_like_stream = file
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, '_file_like_stream'):
1368
- self._file_like_stream.close()
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
- # Clean up reader
1439
- if hasattr(self, '_reader') and self._reader:
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
- print(
1607
+ # Log any unexpected errors during close
1608
+ logger.error(
1476
1609
  Reader._ERROR_MESSAGES['cleanup_error'].format(
1477
- str(e)), file=sys.stderr)
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
- if not self._reader:
1492
- raise C2paError("Reader is closed")
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
- if not self._reader:
1517
- raise C2paError("Reader is closed")
1649
+ self._ensure_valid_state()
1518
1650
 
1519
- self._uri_str = uri.encode('utf-8')
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, self._uri_str, stream_obj._stream)
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
- print(
1824
+ logger.error(
1683
1825
  cls._ERROR_MESSAGES['callback_error'].format(
1684
- str(e)), file=sys.stderr)
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
- if self._closed:
1732
- raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
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
- Errors during cleanup are logged but not raised to ensure cleanup.
1745
- Multiple calls to close() are handled gracefully.
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 self._signer:
1752
- try:
1753
- _lib.c2pa_signer_free(self._signer)
1754
- except Exception as e:
1755
- print(
1756
- Signer._ERROR_MESSAGES['signer_cleanup'].format(
1757
- str(e)), file=sys.stderr)
1758
- finally:
1759
- self._signer = None
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
- print(
1946
+ # Log any unexpected errors during close
1947
+ logger.error(
1762
1948
  Signer._ERROR_MESSAGES['cleanup_error'].format(
1763
- str(e)), file=sys.stderr)
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
- if self._closed or not self._signer:
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
- # CDecode values to place them in Python managed memory
1831
- result = [arr[i].decode("utf-8") for i in range(count.value)]
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
- # Release native memory, as per API contract
1834
- # c2pa_builder_supported_mime_types must call
1835
- # c2pa_free_string_array
1836
- _lib.c2pa_free_string_array(arr, count.value)
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
- if hasattr(self, '_closed'):
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
- # Clean up builder
1944
- if hasattr(self, '_builder') and self._builder:
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
- print(
2213
+ # Log any unexpected errors during close
2214
+ logger.error(
1955
2215
  Builder._ERROR_MESSAGES['cleanup_error'].format(
1956
- str(e)), file=sys.stderr)
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
- if not self._builder:
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
- if not self._builder:
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
- if not self._builder:
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
- if format not in Builder.get_supported_mime_types():
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
- result = _lib.c2pa_builder_sign(
2206
- self._builder,
2207
- format_str,
2208
- source_stream._stream,
2209
- dest_stream._stream,
2210
- signer._signer,
2211
- ctypes.byref(manifest_bytes_ptr)
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
- # Ignore errors during cleanup
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 to avoid interfering with global configuration
20
- logger = logging.getLogger("c2pa")
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
- f"Could not find {lib_name} in any of the search paths: {[str(p) for p in possible_paths]}")
246
- logger.error(
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.17.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