Skip to content

Commit f5e1337

Browse files
authored
Add files via upload
1 parent 5ba73bd commit f5e1337

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

pyarchivefile.py

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import time
2626
import stat
2727
import zlib
28+
import mmap
2829
import base64
2930
import shutil
3031
import socket
@@ -97,6 +98,14 @@
9798
except NameError: # Py3
9899
unicode = str
99100

101+
if PY2:
102+
# In Py2, 'str' is bytes; define a 'bytes' alias for clarity
103+
bytes_type = str
104+
text_type = unicode # noqa: F821 (Py2-only)
105+
else:
106+
bytes_type = bytes
107+
text_type = str
108+
100109
def to_text(s, encoding="utf-8", errors="ignore"):
101110
if s is None:
102111
return u""
@@ -163,6 +172,12 @@ def _wrap(stream):
163172
except NameError:
164173
FileNotFoundError = IOError
165174

175+
try:
176+
UnsupportedOperation = io.UnsupportedOperation # Py3
177+
except AttributeError:
178+
class UnsupportedOperation(IOError): # Py2 fallback
179+
pass
180+
166181
# RAR file support
167182
rarfile_support = False
168183
try:
@@ -5786,6 +5801,310 @@ def UncompressBytesAltFP(fp, formatspecs=__file_format_multi_dict__, filestart=0
57865801
filefp.seek(0, 0)
57875802
return filefp
57885803

5804+
def _extract_base_fp(obj):
5805+
"""Return deepest file-like with a working fileno(), or None."""
5806+
seen = set()
5807+
cur = obj
5808+
while cur and id(cur) not in seen:
5809+
seen.add(id(cur))
5810+
fileno = getattr(cur, "fileno", None)
5811+
if callable(fileno):
5812+
try:
5813+
fileno() # probe
5814+
return cur
5815+
except Exception:
5816+
pass
5817+
# Walk common wrappers (gzip/bz2/lzma/TextIOWrapper/Buffered* etc.)
5818+
for attr in ("fileobj", "fp", "_fp", "buffer", "raw"):
5819+
nxt = getattr(cur, attr, None)
5820+
if nxt is not None and id(nxt) not in seen:
5821+
cur = nxt
5822+
break
5823+
else:
5824+
cur = None
5825+
return None
5826+
5827+
5828+
class FileLikeAdapter(object):
5829+
"""
5830+
Py2/3-compatible file-like adapter that can wrap:
5831+
- BytesIO / memory buffers
5832+
- real files
5833+
- compressed streams (gzip/bz2/lzma/...)
5834+
- an (fp, mmap) pair for uncompressed random-access speed
5835+
5836+
Bytes-only API. Honors mode ("rb", "wb", "r+b", etc.).
5837+
"""
5838+
5839+
def __init__(self, fp_like, mode="rb", mm=None, name=None):
5840+
self._fp = fp_like # underlying stream (BytesIO/file/gzip/...)
5841+
self._mm = mm # optional memory map for uncompressed files
5842+
self._pos = 0 # mapping position when using _mm
5843+
self._mode = mode
5844+
self.name = name if name is not None else getattr(fp_like, "name", None)
5845+
self._closed = False
5846+
5847+
# permissions (simple flags from mode)
5848+
self._readable = ("r" in mode) or ("+" in mode)
5849+
self._writable = ("w" in mode) or ("a" in mode) or ("x" in mode) or ("+" in mode)
5850+
5851+
# Accept write_through attr; ignore (compat shim)
5852+
self.write_through = False
5853+
5854+
# ---- capability flags ----
5855+
def readable(self):
5856+
return bool(self._readable)
5857+
5858+
def writable(self):
5859+
return bool(self._writable)
5860+
5861+
def seekable(self):
5862+
if self._mm is not None:
5863+
return True
5864+
s = getattr(self._fp, "seekable", None)
5865+
if callable(s):
5866+
try:
5867+
return bool(s())
5868+
except Exception:
5869+
return hasattr(self._fp, "seek")
5870+
return hasattr(self._fp, "seek")
5871+
5872+
@property
5873+
def closed(self):
5874+
base_closed = getattr(self._fp, "closed", None)
5875+
return bool(base_closed) or self._closed
5876+
5877+
# ---- position ----
5878+
def tell(self):
5879+
if self._mm is not None:
5880+
return self._pos
5881+
return self._fp.tell()
5882+
5883+
def seek(self, offset, whence=io.SEEK_SET):
5884+
if self._mm is None:
5885+
return self._fp.seek(offset, whence)
5886+
5887+
if whence == io.SEEK_SET:
5888+
new = offset
5889+
elif whence == io.SEEK_CUR:
5890+
new = self._pos + offset
5891+
elif whence == io.SEEK_END:
5892+
new = len(self._mm) + offset
5893+
else:
5894+
raise ValueError("bad whence")
5895+
5896+
if not (0 <= new <= len(self._mm)):
5897+
raise ValueError("seek out of range")
5898+
self._pos = new
5899+
return self._pos
5900+
5901+
# ---- reads ----
5902+
def read(self, n=-1):
5903+
if not self._readable:
5904+
raise UnsupportedOperation("not readable")
5905+
5906+
if self._mm is None:
5907+
return self._fp.read(n)
5908+
5909+
if n is None or n < 0:
5910+
n = len(self._mm) - self._pos
5911+
end = min(self._pos + n, len(self._mm))
5912+
if end <= self._pos:
5913+
return b"" if not PY2 else bytes_type()
5914+
# In Py2, bytes(self._mm[slice]) returns str (bytes); fine.
5915+
out = bytes(self._mm[self._pos:end])
5916+
self._pos = end
5917+
return out
5918+
5919+
def readinto(self, b):
5920+
if not self._readable:
5921+
raise UnsupportedOperation("not readable")
5922+
5923+
if self._mm is None:
5924+
readinto = getattr(self._fp, "readinto", None)
5925+
if callable(readinto):
5926+
return readinto(b)
5927+
# Emulate readinto if missing (common on Py2 wrappers)
5928+
data = self._fp.read(len(b))
5929+
if not data:
5930+
return 0
5931+
mv = memoryview(b)
5932+
n = min(len(mv), len(data))
5933+
mv[:n] = data[:n]
5934+
return n
5935+
5936+
mv = memoryview(b)
5937+
remaining = len(self._mm) - self._pos
5938+
n = min(len(mv), remaining)
5939+
if n <= 0:
5940+
return 0
5941+
mv[:n] = self._mm[self._pos:self._pos + n]
5942+
self._pos += n
5943+
return n
5944+
5945+
def readline(self, limit=-1):
5946+
if not self._readable:
5947+
raise UnsupportedOperation("not readable")
5948+
5949+
if self._mm is None:
5950+
return self._fp.readline(limit)
5951+
5952+
if limit is not None and limit >= 0:
5953+
end_limit = min(self._pos + limit, len(self._mm))
5954+
else:
5955+
end_limit = len(self._mm)
5956+
5957+
nl = self._mm.find(b"\n", self._pos, end_limit)
5958+
if nl == -1:
5959+
end = end_limit
5960+
else:
5961+
end = nl + 1
5962+
out = bytes(self._mm[self._pos:end])
5963+
self._pos = end
5964+
return out
5965+
5966+
def readlines(self, hint=-1):
5967+
lines, total = [], 0
5968+
while True:
5969+
line = self.readline()
5970+
if not line:
5971+
break
5972+
lines.append(line)
5973+
total += len(line)
5974+
if hint >= 0 and total >= hint:
5975+
break
5976+
return lines
5977+
5978+
# Iteration (Py2/3)
5979+
def __iter__(self):
5980+
return self
5981+
5982+
def __next__(self):
5983+
line = self.readline()
5984+
if not line:
5985+
raise StopIteration
5986+
return line
5987+
5988+
# Py2 alias
5989+
if PY2:
5990+
next = __next__
5991+
5992+
# ---- writes ----
5993+
def write(self, b):
5994+
if not self._writable:
5995+
raise UnsupportedOperation("not writable")
5996+
5997+
if not isinstance(b, bytes_type):
5998+
# for safety, only bytes; caller handles text encoding externally
5999+
raise TypeError("write() requires bytes")
6000+
6001+
if self._mm is None:
6002+
return self._fp.write(b)
6003+
6004+
mv = memoryview(b)
6005+
end = self._pos + len(mv)
6006+
if end > len(self._mm):
6007+
raise IOError("write past mapped size; pre-size or resize()")
6008+
self._mm[self._pos:end] = mv
6009+
self._pos = end
6010+
return len(mv)
6011+
6012+
def writelines(self, lines):
6013+
for line in lines:
6014+
self.write(line)
6015+
6016+
# ---- durability & size ----
6017+
def flush(self):
6018+
# 1) flush mapping first
6019+
if self._mm is not None:
6020+
try:
6021+
self._mm.flush()
6022+
except Exception:
6023+
pass
6024+
# 2) flush Python/stdio buffers
6025+
try:
6026+
self._fp.flush()
6027+
except Exception:
6028+
pass
6029+
# 3) fsync real file if any (skips BytesIO and many compressed)
6030+
base = _extract_base_fp(self._fp)
6031+
if base is not None:
6032+
try:
6033+
os.fsync(base.fileno())
6034+
except Exception:
6035+
pass
6036+
6037+
def truncate(self, size=None):
6038+
if self._mm is not None:
6039+
base = _extract_base_fp(self._fp)
6040+
if base is None:
6041+
raise UnsupportedOperation("truncate unsupported for mmapped non-file")
6042+
if size is None:
6043+
size = self.tell()
6044+
# Safe approach across OSes: close map, truncate file, re-map
6045+
was_pos = self._pos
6046+
try:
6047+
self._mm.close()
6048+
except Exception:
6049+
pass
6050+
base.truncate(size)
6051+
access = mmap.ACCESS_WRITE if self._writable else mmap.ACCESS_READ
6052+
self._mm = mmap.mmap(base.fileno(), size, access=access)
6053+
self._pos = min(was_pos, size)
6054+
return size
6055+
6056+
trunc = getattr(self._fp, "truncate", None)
6057+
if not callable(trunc):
6058+
raise UnsupportedOperation("truncate unsupported by underlying object")
6059+
return trunc(size)
6060+
6061+
# ---- fd/tty ----
6062+
def fileno(self):
6063+
f = getattr(self._fp, "fileno", None)
6064+
if callable(f):
6065+
return f()
6066+
raise UnsupportedOperation("no fileno()")
6067+
6068+
def isatty(self):
6069+
f = getattr(self._fp, "isatty", None)
6070+
try:
6071+
return bool(f()) if callable(f) else False
6072+
except Exception:
6073+
return False
6074+
6075+
# ---- close & ctx mgr ----
6076+
def close(self):
6077+
if self._closed:
6078+
return
6079+
try:
6080+
if self._writable:
6081+
self.flush()
6082+
finally:
6083+
if self._mm is not None:
6084+
try:
6085+
self._mm.close()
6086+
except Exception:
6087+
pass
6088+
self._mm = None
6089+
try:
6090+
self._fp.close()
6091+
except Exception:
6092+
pass
6093+
self._closed = True
6094+
6095+
def __enter__(self):
6096+
return self
6097+
6098+
def __exit__(self, exc_type, exc, tb):
6099+
self.close()
6100+
6101+
# Accept write_through sets (compat with your current code)
6102+
def __setattr__(self, name, value):
6103+
if name == "write_through":
6104+
object.__setattr__(self, name, value)
6105+
return
6106+
object.__setattr__(self, name, value)
6107+
57896108

57906109
def CompressOpenFileAlt(fp, compression="auto", compressionlevel=None, compressionuselist=compressionlistalt, formatspecs=__file_format_dict__):
57916110
if(not hasattr(fp, "read")):

0 commit comments

Comments
 (0)