@@ -1102,9 +1102,27 @@ def __exit__(self, exc_type, exc_val, exc_tb):
11021102 self .close ()
11031103
11041104 def __del__ (self ):
1105- """Ensure resources are cleaned up if close() wasn't called."""
1106- if hasattr (self , '_closed' ):
1107- self .close ()
1105+ """Ensure resources are cleaned up if close() wasn't called.
1106+
1107+ This destructor only cleans up if the object hasn't been explicitly closed.
1108+ """
1109+ try :
1110+ # Only cleanup if not already closed and we have a valid stream
1111+ if hasattr (self , '_closed' ) and not self ._closed :
1112+ if hasattr (self , '_stream' ) and self ._stream :
1113+ # Use internal cleanup to avoid calling close() which could cause issues
1114+ try :
1115+ _lib .c2pa_release_stream (self ._stream )
1116+ except Exception :
1117+ # Destructors shouldn't raise exceptions, just log silently
1118+ pass
1119+ finally :
1120+ self ._stream = None
1121+ self ._closed = True
1122+ self ._initialized = False
1123+ except Exception :
1124+ # Destructors must not raise exceptions
1125+ pass
11081126
11091127 def close (self ):
11101128 """Release the stream resources.
@@ -1918,6 +1936,8 @@ def __init__(self, manifest_json: Any):
19181936 C2paError.Encoding: If manifest JSON contains invalid UTF-8 chars
19191937 C2paError.Json: If the manifest JSON cannot be serialized
19201938 """
1939+ # Initialize state immediately to prevent race conditions
1940+ self ._closed = False
19211941 self ._builder = None
19221942
19231943 if not isinstance (manifest_json , str ):
@@ -1981,6 +2001,8 @@ def from_archive(cls, stream: Any) -> 'Builder':
19812001 builder ._builder = _lib .c2pa_builder_from_archive (stream_obj ._stream )
19822002
19832003 if not builder ._builder :
2004+ # Clean up the stream object if builder creation fails
2005+ stream_obj .close ()
19842006 error = _parse_operation_result_for_error (_lib .c2pa_error ())
19852007 if error :
19862008 raise C2paError (error )
@@ -1989,9 +2011,12 @@ def from_archive(cls, stream: Any) -> 'Builder':
19892011 return builder
19902012
19912013 def __del__ (self ):
1992- """Ensure resources are cleaned up if close() wasn't called."""
1993- if hasattr (self , '_closed' ):
1994- self .close ()
2014+ """Ensure resources are cleaned up if close() wasn't called.
2015+
2016+ This destructor safely handles cleanup without causing double frees.
2017+ It only cleans up if the object hasn't been explicitly closed.
2018+ """
2019+ self ._cleanup_resources ()
19952020
19962021 def close (self ):
19972022 """Release the builder resources.
@@ -2001,25 +2026,14 @@ def close(self):
20012026 Errors during cleanup are logged but not raised to ensure cleanup.
20022027 Multiple calls to close() are handled gracefully.
20032028 """
2004- # Track if we've already cleaned up
2005- if not hasattr (self , '_closed' ):
2006- self ._closed = False
2007-
20082029 if self ._closed :
20092030 return
20102031
20112032 try :
2012- # Clean up builder
2013- if hasattr (self , '_builder' ) and self ._builder :
2014- try :
2015- _lib .c2pa_builder_free (self ._builder )
2016- except Exception as e :
2017- print (
2018- Builder ._ERROR_MESSAGES ['builder_cleanup' ].format (
2019- str (e )), file = sys .stderr )
2020- finally :
2021- self ._builder = None
2033+ # Use the internal cleanup method
2034+ self ._cleanup_resources ()
20222035 except Exception as e :
2036+ # Log any unexpected errors during close
20232037 print (
20242038 Builder ._ERROR_MESSAGES ['cleanup_error' ].format (
20252039 str (e )), file = sys .stderr )
@@ -2039,9 +2053,7 @@ def set_no_embed(self):
20392053 into the asset when signing.
20402054 This is useful when creating cloud or sidecar manifests.
20412055 """
2042- if not self ._builder :
2043- raise C2paError (Builder ._ERROR_MESSAGES ['closed_error' ])
2044-
2056+ self ._ensure_valid_state ()
20452057 _lib .c2pa_builder_set_no_embed (self ._builder )
20462058
20472059 def set_remote_url (self , remote_url : str ):
@@ -2056,8 +2068,7 @@ def set_remote_url(self, remote_url: str):
20562068 Raises:
20572069 C2paError: If there was an error setting the remote URL
20582070 """
2059- if not self ._builder :
2060- raise C2paError (Builder ._ERROR_MESSAGES ['closed_error' ])
2071+ self ._ensure_valid_state ()
20612072
20622073 url_str = remote_url .encode ('utf-8' )
20632074 result = _lib .c2pa_builder_set_remote_url (self ._builder , url_str )
@@ -2080,8 +2091,7 @@ def add_resource(self, uri: str, stream: Any):
20802091 Raises:
20812092 C2paError: If there was an error adding the resource
20822093 """
2083- if not self ._builder :
2084- raise C2paError (Builder ._ERROR_MESSAGES ['closed_error' ])
2094+ self ._ensure_valid_state ()
20852095
20862096 uri_str = uri .encode ('utf-8' )
20872097 with Stream (stream ) as stream_obj :
@@ -2392,6 +2402,40 @@ def sign_file(self,
23922402 except Exception as e :
23932403 raise C2paError (f"Error signing file: { str (e )} " ) from e
23942404
2405+ def _ensure_valid_state (self ):
2406+ """Ensure the builder is in a valid state for operations.
2407+
2408+ Raises:
2409+ C2paError: If the builder is closed or invalid
2410+ """
2411+ if self ._closed :
2412+ raise C2paError (Builder ._ERROR_MESSAGES ['closed_error' ])
2413+ if not self ._builder :
2414+ raise C2paError (Builder ._ERROR_MESSAGES ['closed_error' ])
2415+
2416+ def _cleanup_resources (self ):
2417+ """Internal cleanup method that safely releases native resources.
2418+
2419+ This method handles the actual cleanup logic and can be called
2420+ from both close() and __del__ without causing double frees.
2421+ """
2422+ try :
2423+ # Only cleanup if not already closed and we have a valid builder
2424+ if hasattr (self , '_closed' ) and not self ._closed :
2425+ if hasattr (self , '_builder' ) and self ._builder and self ._builder != 0 :
2426+ try :
2427+ _lib .c2pa_builder_free (self ._builder )
2428+ except Exception :
2429+ # Log cleanup errors but don't raise exceptions
2430+ pass
2431+ finally :
2432+ # Always clear the pointer and mark as closed
2433+ self ._builder = None
2434+ self ._closed = True
2435+ except Exception :
2436+ # Ensure we don't raise exceptions during cleanup
2437+ pass
2438+
23952439
23962440def format_embeddable (format : str , manifest_bytes : bytes ) -> tuple [int , bytes ]:
23972441 """Convert a binary C2PA manifest into an embeddable version.
0 commit comments