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