Skip to content

Commit a5ddbfa

Browse files
committed
fix: Increase test coverage, fix accordingly 2
1 parent 4f05cdf commit a5ddbfa

File tree

4 files changed

+111
-81
lines changed

4 files changed

+111
-81
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ publish: release
9292
python3 -m pip install twine
9393
python3 -m twine upload dist/*
9494

95+
# Code analysis
96+
check-format:
97+
flake8 src/c2pa/c2pa.py
98+
9599
# Formats Python source code using autopep8 with aggressive settings
96100
format:
97101
autopep8 --aggressive --aggressive --in-place src/c2pa/*.py

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ requests>=2.0.0
1313

1414
# Code formatting
1515
autopep8==2.0.4 # For automatic code formatting
16+
flake8==7.3.0
1617

1718
# Test dependencies (for callback signers)
1819
cryptography==45.0.4

src/c2pa/build.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ def get_latest_release() -> dict:
2121

2222

2323
def download_artifact(url: str, platform_name: str) -> None:
24-
"""Download and extract an artifact to the appropriate platform directory."""
24+
"""
25+
Download and extract an artifact
26+
to the appropriate platform directory.
27+
"""
2528
print(f"Downloading artifact for {platform_name}...")
2629

2730
# Create platform directory

src/c2pa/c2pa.py

Lines changed: 102 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -459,12 +459,24 @@ class _StringContainer:
459459

460460
def __init__(self):
461461
"""Initialize an empty string container."""
462-
pass
462+
self._path_str = ""
463+
self._data_dir_str = ""
464+
465+
466+
def _convert_to_py_string(value) -> str:
467+
if value is None:
468+
return ""
469+
470+
# Convert to Python string and free the Rust-allocated memory
471+
py_string = ctypes.cast(value, ctypes.c_char_p).value.decode('utf-8')
472+
_lib.c2pa_string_free(value)
473+
474+
return py_string
463475

464476

465477
def _parse_operation_result_for_error(
466-
result: ctypes.c_void_p,
467-
check_error: bool = True) -> Optional[str]:
478+
result: ctypes.c_void_p | None,
479+
check_error: bool = True) -> str | None:
468480
"""Helper function to handle string results from C2PA functions."""
469481
if not result: # pragma: no cover
470482
if check_error:
@@ -509,16 +521,13 @@ def _parse_operation_result_for_error(
509521
return error_str
510522
return None
511523

512-
# Convert to Python string and free the Rust-allocated memory
513-
py_string = ctypes.cast(result, ctypes.c_char_p).value.decode('utf-8')
514-
_lib.c2pa_string_free(result)
515-
516-
return py_string
524+
# In the case result would be a string already (error message)
525+
return _convert_to_py_string(result)
517526

518527

519528
def sdk_version() -> str:
520529
"""
521-
Returns the underlying c2pa-rs version string, e.g., "0.49.5".
530+
Returns the underlying c2pa-rs/c2pa-c-ffi version string
522531
"""
523532
vstr = version()
524533
# Example: "c2pa-c/0.49.5 c2pa-rs/0.49.5"
@@ -532,9 +541,7 @@ def sdk_version() -> str:
532541
def version() -> str:
533542
"""Get the C2PA library version."""
534543
result = _lib.c2pa_version()
535-
py_string = ctypes.cast(result, ctypes.c_char_p).value.decode("utf-8")
536-
_lib.c2pa_string_free(result) # Free the Rust-allocated memory
537-
return py_string
544+
return _convert_to_py_string(result)
538545

539546

540547
def load_settings(settings: str, format: str = "json") -> None:
@@ -555,6 +562,8 @@ def load_settings(settings: str, format: str = "json") -> None:
555562
error = _parse_operation_result_for_error(_lib.c2pa_error())
556563
if error:
557564
raise C2paError(error)
565+
raise C2paError("Error loading settings")
566+
558567
return result
559568

560569

@@ -596,7 +605,16 @@ def read_ingredient_file(
596605

597606
result = _lib.c2pa_read_ingredient_file(
598607
container._path_str, container._data_dir_str)
599-
return _parse_operation_result_for_error(result)
608+
609+
if result is None:
610+
error = _parse_operation_result_for_error(_lib.c2pa_error())
611+
if error:
612+
raise C2paError(error)
613+
raise C2paError(
614+
"Error reading ingredient file {}".format(path)
615+
)
616+
617+
return _convert_to_py_string(result)
600618

601619

602620
def read_file(path: Union[str, Path],
@@ -636,7 +654,15 @@ def read_file(path: Union[str, Path],
636654
container._data_dir_str = str(data_dir).encode('utf-8')
637655

638656
result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
639-
return _parse_operation_result_for_error(result)
657+
if result is None:
658+
error = _parse_operation_result_for_error(_lib.c2pa_error())
659+
if error is not None:
660+
raise C2paError(error)
661+
raise C2paError.Other(
662+
"Error during read of manifest from file {}".format(path)
663+
)
664+
665+
return _convert_to_py_string(result)
640666

641667

642668
@overload
@@ -726,41 +752,27 @@ def sign_file(
726752
raise C2paError.NotSupported(
727753
f"Could not determine MIME type for file: {source_path}")
728754

729-
if return_manifest_as_bytes:
730-
# Convert Python streams to Stream objects for internal signing
731-
source_stream = Stream(source_file)
732-
# In-memory buffer stream that will be flushed to file
733-
dest_stream = Stream(io.BytesIO(bytearray()))
755+
# Convert Python streams to Stream objects for internal signing
756+
source_stream = Stream(source_file)
757+
# In-memory buffer stream that will be flushed to file
758+
dest_stream = Stream(io.BytesIO(bytearray()))
734759

735-
# Use the builder's internal signing logic to get manifest
736-
# bytes
737-
manifest_bytes = builder._sign_internal(
738-
signer, mime_type, source_stream, dest_stream)
760+
# Use the builder's internal signing logic to get manifest
761+
# bytes
762+
manifest_bytes = builder._sign_internal(
763+
signer, mime_type, source_stream, dest_stream)
739764

740-
source_stream.close()
765+
source_stream.close()
741766

742-
# Write the signed content from BytesIO to the destination file
743-
dest_stream._file_like_stream.seek(0) # Reset to beginning of stream
744-
with open(dest_path, 'wb') as dest_file:
745-
dest_file.write(dest_stream._file_like_stream.getvalue())
746-
dest_stream.close()
767+
# Write the in-memory signed content
768+
# from BytesIO to the destination file
769+
with open(dest_path, 'wb') as dest_file:
770+
dest_stream.write_to_target(dest_file)
771+
dest_stream.close()
747772

773+
if return_manifest_as_bytes:
748774
return manifest_bytes
749775
else:
750-
# Sign the file using the builder with BytesIO destination
751-
dest_stream = io.BytesIO(bytearray())
752-
builder.sign(
753-
signer=signer,
754-
format=mime_type,
755-
source=source_file,
756-
dest=dest_stream
757-
)
758-
759-
# Write the signed content from BytesIO to the destination file
760-
dest_stream.seek(0) # Reset to beginning of stream
761-
with open(dest_path, 'wb') as dest_file:
762-
dest_file.write(dest_stream.getvalue())
763-
764776
# Read the signed manifest from the destination file
765777
with Reader(dest_path) as reader:
766778
return reader.json()
@@ -1056,6 +1068,10 @@ def close(self):
10561068
self._closed = True
10571069
self._initialized = False
10581070

1071+
def write_to_target(self, dest_stream):
1072+
self._file_like_stream.seek(0)
1073+
dest_stream.write(self._file_like_stream.getvalue())
1074+
10591075
@property
10601076
def closed(self) -> bool:
10611077
"""Check if the stream is closed.
@@ -1202,7 +1218,9 @@ def __init__(self,
12021218
try:
12031219
file = open(stream, 'rb')
12041220
self._own_stream = Stream(file)
1205-
self._format_str = format_or_path.encode('utf-8')
1221+
1222+
format_str = str(format_or_path)
1223+
self._format_str = format_str.encode('utf-8')
12061224

12071225
if manifest_data is None:
12081226
self._reader = _lib.c2pa_reader_from_stream(
@@ -1248,13 +1266,14 @@ def __init__(self,
12481266
Reader._ERROR_MESSAGES['io_error'].format(
12491267
str(e)))
12501268
else:
1251-
# format_or_path is a format
1252-
if format_or_path not in Reader.get_supported_mime_types():
1269+
# format_or_path is a format string
1270+
format_str = str(format_or_path)
1271+
if format_str not in Reader.get_supported_mime_types():
12531272
raise C2paError.NotSupported(
1254-
f"Reader does not support {format_or_path}")
1273+
f"Reader does not support {format_str}")
12551274

12561275
# Use the provided stream
1257-
self._format_str = format_or_path.encode('utf-8')
1276+
self._format_str = format_str.encode('utf-8')
12581277

12591278
with Stream(stream) as stream_obj:
12601279
if manifest_data is None:
@@ -1368,7 +1387,14 @@ def json(self) -> str:
13681387
if not self._reader:
13691388
raise C2paError("Reader is closed")
13701389
result = _lib.c2pa_reader_json(self._reader)
1371-
return _parse_operation_result_for_error(result)
1390+
1391+
if result is None:
1392+
error = _parse_operation_result_for_error(_lib.c2pa_error())
1393+
if error:
1394+
raise C2paError(error)
1395+
raise C2paError("Error during manifest parsing in Reader")
1396+
1397+
return _convert_to_py_string(result)
13721398

13731399
def resource_to_stream(self, uri: str, stream: Any) -> int:
13741400
"""Write a resource to a stream.
@@ -1395,6 +1421,9 @@ def resource_to_stream(self, uri: str, stream: Any) -> int:
13951421
error = _parse_operation_result_for_error(_lib.c2pa_error())
13961422
if error:
13971423
raise C2paError(error)
1424+
raise C2paError.Other(
1425+
"Error during resource {} to stream conversion".format(uri)
1426+
)
13981427

13991428
return result
14001429

@@ -1481,7 +1510,7 @@ def from_callback(
14811510
)
14821511
)
14831512

1484-
if tsa_url and not tsa_url.startswith(('http://', 'https://')):
1513+
if tsa_url and not tsa_url.startswith(("http://", "https://")):
14851514
raise C2paError(
14861515
cls._ERROR_MESSAGES['invalid_tsa'].format(
14871516
"Invalid TSA URL format"
@@ -2006,7 +2035,7 @@ def add_ingredient_from_file_path(
20062035
except Exception as e:
20072036
raise C2paError.Other(f"Could not add ingredient: {e}") from e
20082037

2009-
def to_archive(self, stream: Any):
2038+
def to_archive(self, stream: Any) -> None:
20102039
"""Write an archive of the builder to a stream.
20112040
20122041
Args:
@@ -2078,6 +2107,7 @@ def _sign_internal(
20782107
error = _parse_operation_result_for_error(_lib.c2pa_error())
20792108
if error:
20802109
raise C2paError(error)
2110+
raise C2paError("Error during signing")
20812111

20822112
# Capture the manifest bytes if available
20832113
manifest_bytes = b""
@@ -2100,17 +2130,14 @@ def _sign_internal(
21002130
# Ignore errors during cleanup
21012131
pass
21022132

2103-
return manifest_bytes
2104-
2105-
# exception will propagate after finally block execution,
2106-
# caller to handle them
2133+
return manifest_bytes
21072134

21082135
def sign(
21092136
self,
21102137
signer: Signer,
21112138
format: str,
21122139
source: Any,
2113-
dest: Any = None) -> None:
2140+
dest: Any = None) -> bytes:
21142141
"""Sign the builder's content and write to a destination stream.
21152142
21162143
Args:
@@ -2125,10 +2152,14 @@ def sign(
21252152
# Convert Python streams to Stream objects
21262153
source_stream = Stream(source)
21272154

2128-
# Use a BytesIO Stream to ensure proper
2129-
# APIs an interfaces are available for stream manipulation
2130-
mem_buffer = io.BytesIO()
2131-
dest_stream = Stream(mem_buffer)
2155+
if dest:
2156+
# dest is optional, only if we write back somewhere
2157+
dest_stream = Stream(dest)
2158+
else:
2159+
# no destination?
2160+
# we keep things in-memory for validation and processing
2161+
mem_buffer = io.BytesIO()
2162+
dest_stream = Stream(mem_buffer)
21322163

21332164
# Use the internal stream-base signing logic
21342165
manifest_bytes = self._sign_internal(
@@ -2138,19 +2169,9 @@ def sign(
21382169
dest_stream
21392170
)
21402171

2141-
# This is needed because not all streams have the same API,
2142-
# so we need to ensure we use the right ones at the right time
2143-
# and with the proper function calls.
2144-
mem_buffer.seek(0)
2145-
# Write memory buffer content to the opened destination
2146-
# (may be a file stream or an in-memory stream).
2147-
# This allows to use file streams and in-memory streams
2148-
# interchangeably...
2149-
if dest:
2150-
# Only if dest is truthy, write back
2151-
dest.write(mem_buffer.getvalue())
2152-
dest.flush()
2153-
dest_stream.close()
2172+
if not dest:
2173+
# Close temporary in-memory stream since we own it
2174+
dest_stream.close()
21542175

21552176
return manifest_bytes
21562177

@@ -2182,22 +2203,23 @@ def sign_file(self,
21822203
try:
21832204
# Open source file and create BytesIO for destination
21842205
with open(source_path, 'rb') as source_file:
2185-
# Convert Python streams to Stream objects
21862206
source_stream = Stream(source_file)
2187-
# In-memory buffer stream that will be flushed to target
2207+
2208+
# In-memory buffer stream that will be flushed to target,
2209+
# to ensure proper in-memory processing and validatin
21882210
dest_stream = Stream(io.BytesIO(bytearray()))
21892211

2190-
# Use the internal stream-base signing logic
2191-
# Sign the content to the BytesIO stream
2212+
# Use the internal stream-base signing logic to
2213+
# sign the content to the BytesIO stream
21922214
result = self._sign_internal(
21932215
signer, mime_type, source_stream, dest_stream)
21942216

21952217
source_stream.close()
21962218

2197-
# Write the signed content from BytesIO to the destination file
2198-
dest_stream._file_like_stream.seek(0) # Reset to beginning of stream
2219+
# Write the signed content from in-memory stream
2220+
# to the destination file
21992221
with open(dest_path, 'wb') as dest_file:
2200-
dest_file.write(dest_stream._file_like_stream.getvalue())
2222+
dest_stream.write_to_target(dest_file)
22012223
dest_stream.close()
22022224

22032225
return result

0 commit comments

Comments
 (0)