1+ import logging
12import os
23import zlib
34from io import BufferedIOBase
45
56from rpycdec .safe_pickle import rpa_loads
7+ from rpycdec .utils import safe_path
68
9+ logger = logging .getLogger (__name__ )
710
8- def read_util (data : BufferedIOBase , util : int = 0x00 ) -> bytes :
11+
12+ def read_until (data : BufferedIOBase , delimiter : int = 0x00 ) -> bytes :
13+ """Read bytes from stream until delimiter is found.
14+
15+ Raises ValueError on unexpected EOF.
16+ """
917 content = bytearray ()
1018 while True :
1119 c = data .read (1 )
12- if c [0 ] == util :
20+ if not c :
21+ raise ValueError ("Unexpected EOF while reading stream" )
22+ if c [0 ] == delimiter :
1323 break
1424 content += c
1525 return content
@@ -23,14 +33,13 @@ def start_to_bytes(left: list | None) -> bytes:
2333 return left [0 ].encode ("latin-1" )
2434
2535
26- def extract_rpa (r : BufferedIOBase , dir : str | None = None ):
27- dir = dir or "."
28- magic = read_util (r , 0x20 )
36+ def extract_rpa (r : BufferedIOBase , output_dir : str | None = None ):
37+ output_dir = output_dir or "."
38+ magic = read_until (r , 0x20 )
2939 if magic != b"RPA-3.0" :
30- print ("Not a Ren'Py archive." )
31- return
32- index_offset = int (read_util (r , 0x20 ), 16 )
33- key = int (read_util (r , 0x0A ).decode (), 16 )
40+ raise ValueError ("Not a Ren'Py RPA-3.0 archive." )
41+ index_offset = int (read_until (r , 0x20 ), 16 )
42+ key = int (read_until (r , 0x0A ).decode (), 16 )
3443
3544 # read index
3645 r .seek (index_offset )
@@ -43,6 +52,10 @@ def extract_rpa(r: BufferedIOBase, dir: str | None = None):
4352 ]
4453
4554 for filename , entries in index .items ():
55+ # Handle bytes filenames from Python 2 era archives
56+ if isinstance (filename , bytes ):
57+ filename = filename .decode ("utf-8" , errors = "surrogateescape" )
58+
4659 data = bytearray ()
4760 for offset , dlen , start in entries :
4861 r .seek (offset )
@@ -51,11 +64,19 @@ def extract_rpa(r: BufferedIOBase, dir: str | None = None):
5164 if block .startswith (start ):
5265 block = block [len (start ) :]
5366 else :
54- print ("Warning: %s does not start with %s" % (filename , start ))
67+ logger .warning (
68+ "%s does not start with expected prefix %s" , filename , start
69+ )
5570 data += block
5671
57- filename = os .path .join (dir , filename )
58- os .makedirs (os .path .dirname (filename ), exist_ok = True )
59- with open (filename , "wb" ) as f :
60- print ("extracting: " , filename )
72+ # Path traversal protection
73+ try :
74+ dest = safe_path (output_dir , filename )
75+ except ValueError :
76+ logger .warning ("Skipping path traversal attempt: %s" , filename )
77+ continue
78+
79+ os .makedirs (os .path .dirname (dest ), exist_ok = True )
80+ with open (dest , "wb" ) as f :
81+ logger .info ("extracting: %s" , dest )
6182 f .write (data )
0 commit comments