@@ -264,10 +264,12 @@ mod _pyexpat {
264264 // drain.
265265 #[ pytraverse( skip) ]
266266 busy : AtomicBool ,
267- // Set once EndDocument has been observed, so further Parse() calls
268- // become harmless no-ops instead of starting a new document.
267+ // Prevent creating another backend after finalization or fork failure.
269268 #[ pytraverse( skip) ]
270269 finished : AtomicBool ,
270+ // Distinguish successful completion from the existing failure paths.
271+ #[ pytraverse( skip) ]
272+ completed : AtomicBool ,
271273 #[ pytraverse( skip) ]
272274 namespace_stack : PyRwLock < Vec < ( xml:: namespace:: Namespace , Vec < String > ) > > ,
273275 #[ pytraverse( skip) ]
@@ -946,6 +948,7 @@ mod _pyexpat {
946948 backend : PyRwLock :: new ( None ) ,
947949 busy : AtomicBool :: new ( false ) ,
948950 finished : AtomicBool :: new ( false ) ,
951+ completed : AtomicBool :: new ( false ) ,
949952 namespace_stack : PyRwLock :: new ( Vec :: new ( ) ) ,
950953 reparse_deferral_enabled : AtomicBool :: new ( true ) ,
951954 parameter_entity_parsing : AtomicI32 :: new ( XML_PARAM_ENTITY_PARSING_NEVER ) ,
@@ -1636,6 +1639,7 @@ mod _pyexpat {
16361639 continue ;
16371640 }
16381641 ParserMsg :: Finished => {
1642+ self . completed . store ( true , Ordering :: SeqCst ) ;
16391643 self . finished . store ( true , Ordering :: SeqCst ) ;
16401644 self . teardown_backend ( ) ;
16411645 return Ok ( ( ) ) ;
@@ -1751,16 +1755,25 @@ mod _pyexpat {
17511755 }
17521756 }
17531757 if isfinal {
1754- // The caller has declared there is no more data coming, so
1755- // (whether or not this re-parse actually reached
1756- // `EndDocument`) further Parse()/ParseFile() calls become
1757- // no-ops, matching libexpat/`Backend::Threaded`.
1758+ // Remember successful completion separately from errors;
1759+ // both close this backend, but only success means the parser
1760+ // should report XML_ERROR_FINISHED on a later call.
1761+ if result. is_ok ( ) {
1762+ self . completed . store ( true , Ordering :: SeqCst ) ;
1763+ }
17581764 self . finished . store ( true , Ordering :: SeqCst ) ;
17591765 self . teardown_backend ( ) ;
17601766 }
17611767 result
17621768 }
17631769
1770+ fn check_completed ( & self , vm : & VirtualMachine ) -> PyResult < ( ) > {
1771+ if self . completed . load ( Ordering :: SeqCst ) {
1772+ return Err ( self . handler_error ( vm, 36 , "parsing finished" ) ?) ;
1773+ }
1774+ Ok ( ( ) )
1775+ }
1776+
17641777 /// Feed another chunk of the document to the (possibly newly
17651778 /// created) backend and dispatch whatever events that unblocks.
17661779 /// `Parse()` and `ParseFile()` may each be called several times
@@ -1804,9 +1817,8 @@ mod _pyexpat {
18041817 ) ) ) ;
18051818 }
18061819 self . started . store ( true , Ordering :: Relaxed ) ;
1820+ self . check_completed ( vm) ?;
18071821 if self . finished . load ( Ordering :: SeqCst ) {
1808- // The document already ended; further data is ignored, same
1809- // as libexpat happily no-oping once XML_Parse has seen EOF.
18101822 return Ok ( ( ) ) ;
18111823 }
18121824 {
@@ -1870,7 +1882,11 @@ mod _pyexpat {
18701882
18711883 #[ pymethod( name = "ParseFile" ) ]
18721884 fn parse_file ( zelf : & Py < Self > , file : PyObjectRef , vm : & VirtualMachine ) -> PyResult < i32 > {
1873- let read_res = vm. call_method ( & file, "read" , ( ) ) ?;
1885+ let read = vm
1886+ . get_attribute_opt ( & file, "read" ) ?
1887+ . ok_or_else ( || vm. new_type_error ( "argument must have 'read' attribute" ) ) ?;
1888+ zelf. check_completed ( vm) ?;
1889+ let read_res = read. call ( ( ) , vm) ?;
18741890 let bytes_like = ArgBytesLike :: try_from_object ( vm, read_res) ?;
18751891 let buf = bytes_like. borrow_buf ( ) . to_vec ( ) ;
18761892 // `file.read()` with no argument reads to EOF, so this chunk is
0 commit comments