@@ -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
465477def _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
519528def 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:
532541def 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
540547def 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
602620def 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