1010from __future__ import annotations
1111
1212import io
13- import os
1413from abc import ABC , abstractmethod
1514from dataclasses import dataclass , field
1615from pathlib import Path
1716from typing import Any
1817
1918import pandas as pd
2019
20+ _MAX_XML_BYTES = 10 * 1024 * 1024
21+
2122
2223@dataclass
2324class ParseResult :
@@ -76,10 +77,10 @@ class Parser(ABC):
7677
7778 @abstractmethod
7879 def parse (self , source : Any ) -> ParseResult :
79- """Parse *source* (path , text, bytes, or file-like) into a :class:`ParseResult`."""
80+ """Parse *source* (Path , text, bytes, or file-like) into a :class:`ParseResult`."""
8081
8182 def read_text (self , source : Any , * , encoding : str = "utf-8" ) -> str :
82- """Read *source* into text, accepting a path , str content, bytes, or file-like."""
83+ """Read *source* into text, accepting a Path , str content, bytes, or file-like."""
8384 if isinstance (source , (bytes , bytearray )):
8485 return bytes (source ).decode (encoding )
8586 if hasattr (source , "read" ):
@@ -88,11 +89,6 @@ def read_text(self, source: Any, *, encoding: str = "utf-8") -> str:
8889 if isinstance (source , Path ):
8990 return source .read_text (encoding = encoding )
9091 if isinstance (source , str ):
91- # A short string that names an existing file is treated as a path;
92- # otherwise it is treated as the content itself.
93- if (len (source ) < 4096 and "\n " not in source and "\r " not in source
94- and os .path .exists (source )):
95- return Path (source ).read_text (encoding = encoding )
9692 return source
9793 raise TypeError (f"cannot read a { type (source ).__name__ } source" )
9894
@@ -104,8 +100,29 @@ def open_binary(self, source: Any) -> io.BufferedIOBase | io.BytesIO:
104100 data = source .read ()
105101 return io .BytesIO (data if isinstance (data , (bytes , bytearray ))
106102 else str (data ).encode ("utf-8" ))
107- if isinstance (source , ( str , Path )) and os . path . exists ( str ( source ) ):
103+ if isinstance (source , Path ):
108104 return open (source , "rb" ) # noqa: SIM115 - caller consumes immediately
109- if isinstance (source , ( str , Path ) ):
105+ if isinstance (source , str ):
110106 return io .BytesIO (str (source ).encode ("utf-8" ))
111107 raise TypeError (f"cannot open a { type (source ).__name__ } source" )
108+
109+ def open_safe_xml_binary (
110+ self ,
111+ source : Any ,
112+ * ,
113+ max_bytes : int = _MAX_XML_BYTES ,
114+ ) -> io .BytesIO :
115+ """Return bounded XML bytes with DTD/entity declarations rejected."""
116+ stream = self .open_binary (source )
117+ try :
118+ data = stream .read (max_bytes + 1 )
119+ finally :
120+ if hasattr (stream , "close" ):
121+ stream .close ()
122+
123+ if len (data ) > max_bytes :
124+ raise ValueError (f"XML input exceeds { max_bytes } bytes" )
125+ lowered = data .lower ()
126+ if b"<!doctype" in lowered or b"<!entity" in lowered :
127+ raise ValueError ("XML DTD/entity declarations are not allowed" )
128+ return io .BytesIO (data )
0 commit comments