PyCatFile 0.22.2__py3-none-any.whl → 0.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycatfile.py CHANGED
@@ -14,7 +14,7 @@
14
14
  Copyright 2018-2024 Game Maker 2k - http://intdb.sourceforge.net/
15
15
  Copyright 2018-2024 Kazuki Przyborowski - https://github.com/KazukiPrzyborowski
16
16
 
17
- $FileInfo: pycatfile.py - Last Update: 8/29/2025 Ver. 0.22.2 RC 1 - Author: cooldude2k $
17
+ $FileInfo: pycatfile.py - Last Update: 10/1/2025 Ver. 0.23.0 RC 1 - Author: cooldude2k $
18
18
  '''
19
19
 
20
20
  from __future__ import absolute_import, division, print_function, unicode_literals, generators, with_statement, nested_scopes
@@ -27,8 +27,8 @@ import stat
27
27
  import zlib
28
28
  import base64
29
29
  import shutil
30
- import struct
31
30
  import socket
31
+ import struct
32
32
  import hashlib
33
33
  import inspect
34
34
  import datetime
@@ -79,6 +79,7 @@ try:
79
79
  except NameError:
80
80
  basestring = str
81
81
 
82
+ PY2 = (sys.version_info[0] == 2)
82
83
  try:
83
84
  unicode # Py2
84
85
  except NameError: # Py3
@@ -271,8 +272,8 @@ def get_default_threads():
271
272
 
272
273
 
273
274
  __use_pysftp__ = False
274
- __upload_proto_support__ = "^(ftp|ftps|sftp|scp)://"
275
- __download_proto_support__ = "^(http|https|ftp|ftps|sftp|scp)://"
275
+ __upload_proto_support__ = "^(ftp|ftps|sftp|scp|tcp|udp)://"
276
+ __download_proto_support__ = "^(http|https|ftp|ftps|sftp|scp|tcp|udp)://"
276
277
  if(not havepysftp):
277
278
  __use_pysftp__ = False
278
279
  __use_http_lib__ = "httpx"
@@ -392,12 +393,12 @@ __file_format_extension__ = __file_format_multi_dict__[__file_format_default__][
392
393
  __file_format_dict__ = __file_format_multi_dict__[__file_format_default__]
393
394
  __project__ = __program_name__
394
395
  __project_url__ = "https://github.com/GameMaker2k/PyCatFile"
395
- __version_info__ = (0, 22, 2, "RC 1", 1)
396
- __version_date_info__ = (2025, 9, 29, "RC 1", 1)
396
+ __version_info__ = (0, 23, 0, "RC 1", 1)
397
+ __version_date_info__ = (2025, 10, 1, "RC 1", 1)
397
398
  __version_date__ = str(__version_date_info__[0]) + "." + str(
398
399
  __version_date_info__[1]).zfill(2) + "." + str(__version_date_info__[2]).zfill(2)
399
400
  __revision__ = __version_info__[3]
400
- __revision_id__ = "$Id: 5942aea043d080ea8842152fa874ad3c7bbb4cc4 $"
401
+ __revision_id__ = "$Id: c566041792a64657ff1e9e2819f10744433f7a11 $"
401
402
  if(__version_info__[4] is not None):
402
403
  __version_date_plusrc__ = __version_date__ + \
403
404
  "-" + str(__version_date_info__[4])
@@ -409,15 +410,67 @@ if(__version_info__[3] is not None):
409
410
  if(__version_info__[3] is None):
410
411
  __version__ = str(__version_info__[0]) + "." + str(__version_info__[1]) + "." + str(__version_info__[2])
411
412
 
413
+ # ===== Module-level type code table & helpers (reuse anywhere) =====
414
+
415
+ FT = {
416
+ "FILE": 0,
417
+ "HARDLINK": 1,
418
+ "SYMLINK": 2,
419
+ "CHAR": 3,
420
+ "BLOCK": 4,
421
+ "DIR": 5,
422
+ "FIFO": 6,
423
+ "CONTAGIOUS": 7, # treated like regular file
424
+ "SOCK": 8,
425
+ "DOOR": 9,
426
+ "PORT": 10,
427
+ "WHT": 11,
428
+ "SPARSE": 12,
429
+ "JUNCTION": 13,
430
+ }
431
+
432
+ BASE_CATEGORY_BY_CODE = {
433
+ 0: "files",
434
+ 1: "hardlinks",
435
+ 2: "symlinks",
436
+ 3: "characters",
437
+ 4: "blocks",
438
+ 5: "directories",
439
+ 6: "fifos",
440
+ 7: "files", # contagious treated as file
441
+ 8: "sockets",
442
+ 9: "doors",
443
+ 10: "ports",
444
+ 11: "whiteouts",
445
+ 12: "sparsefiles",
446
+ 13: "junctions",
447
+ }
448
+
449
+ # Union categories defined by which base codes should populate them.
450
+ UNION_RULES = [
451
+ ("links", set([FT["HARDLINK"], FT["SYMLINK"]])),
452
+ ("devices", set([FT["CHAR"], FT["BLOCK"]])),
453
+ ]
454
+
455
+ # Deterministic category order (handy for consistent output/printing).
456
+ CATEGORY_ORDER = [
457
+ "files", "hardlinks", "symlinks", "character", "block",
458
+ "directories", "fifo", "sockets", "doors", "ports",
459
+ "whiteouts", "sparsefiles", "junctions", "links", "devices"
460
+ ]
461
+
412
462
  # Robust bitness detection
413
463
  # Works on Py2 & Py3, all platforms
464
+
465
+ # Python interpreter bitness
466
+ PyBitness = "64" if struct.calcsize("P") * 8 == 64 else ("64" if sys.maxsize > 2**32 else "32")
467
+
468
+ # Operating system bitness
414
469
  try:
415
- import struct
416
- PyBitness = "64" if struct.calcsize("P") * 8 == 64 else "32"
470
+ OSBitness = platform.architecture()[0].replace("bit", "")
417
471
  except Exception:
418
- # conservative fallback
419
- m = platform.machine() or ""
420
- PyBitness = "64" if m.endswith("64") else "32"
472
+ m = platform.machine().lower()
473
+ OSBitness = "64" if "64" in m else "32"
421
474
 
422
475
  geturls_ua_pyfile_python = "Mozilla/5.0 (compatible; {proname}/{prover}; +{prourl})".format(
423
476
  proname=__project__, prover=__version__, prourl=__project_url__)
@@ -658,6 +711,415 @@ def _resolves_outside(base_rel, target_rel):
658
711
  return True
659
712
 
660
713
 
714
+ def _to_bytes(data):
715
+ if data is None:
716
+ return b""
717
+ if isinstance(data, bytes):
718
+ return data
719
+ if isinstance(data, unicode):
720
+ return data.encode("utf-8")
721
+ try:
722
+ return bytes(data)
723
+ except Exception:
724
+ return (u"%s" % data).encode("utf-8")
725
+
726
+ def _to_text(b):
727
+ if isinstance(b, bytes):
728
+ return b.decode("utf-8", "replace")
729
+ return b
730
+
731
+ # ---------- TLS helpers (TCP only) ----------
732
+ def _ssl_available():
733
+ try:
734
+ import ssl # noqa
735
+ return True
736
+ except Exception:
737
+ return False
738
+
739
+ def _build_ssl_context(server_side=False, verify=True, ca_file=None, certfile=None, keyfile=None):
740
+ import ssl
741
+ create_ctx = getattr(ssl, "create_default_context", None)
742
+ SSLContext = getattr(ssl, "SSLContext", None)
743
+ Purpose = getattr(ssl, "Purpose", None)
744
+ if create_ctx and Purpose:
745
+ ctx = create_ctx(ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH)
746
+ elif SSLContext:
747
+ ctx = SSLContext(getattr(ssl, "PROTOCOL_TLS", getattr(ssl, "PROTOCOL_SSLv23")))
748
+ else:
749
+ return None
750
+
751
+ if hasattr(ctx, "check_hostname") and not server_side:
752
+ ctx.check_hostname = bool(verify)
753
+
754
+ if verify:
755
+ if hasattr(ctx, "verify_mode"):
756
+ ctx.verify_mode = getattr(ssl, "CERT_REQUIRED", 2)
757
+ if ca_file:
758
+ try: ctx.load_verify_locations(cafile=ca_file)
759
+ except Exception: pass
760
+ else:
761
+ load_default_certs = getattr(ctx, "load_default_certs", None)
762
+ if load_default_certs: load_default_certs()
763
+ else:
764
+ if hasattr(ctx, "verify_mode"):
765
+ ctx.verify_mode = getattr(ssl, "CERT_NONE", 0)
766
+ if hasattr(ctx, "check_hostname"):
767
+ ctx.check_hostname = False
768
+
769
+ if certfile:
770
+ ctx.load_cert_chain(certfile=certfile, keyfile=keyfile or None)
771
+
772
+ try:
773
+ ctx.set_ciphers("HIGH:!aNULL:!MD5:!RC4")
774
+ except Exception:
775
+ pass
776
+ return ctx
777
+
778
+ def _ssl_wrap_socket(sock, server_side=False, server_hostname=None,
779
+ verify=True, ca_file=None, certfile=None, keyfile=None):
780
+ import ssl
781
+ ctx = _build_ssl_context(server_side, verify, ca_file, certfile, keyfile)
782
+ if ctx is not None:
783
+ kwargs = {}
784
+ if not server_side and getattr(ssl, "HAS_SNI", False) and server_hostname:
785
+ kwargs["server_hostname"] = server_hostname
786
+ return ctx.wrap_socket(sock, server_side=server_side, **kwargs)
787
+ # Very old Python fallback
788
+ kwargs = {
789
+ "ssl_version": getattr(ssl, "PROTOCOL_TLS", getattr(ssl, "PROTOCOL_SSLv23")),
790
+ "certfile": certfile or None,
791
+ "keyfile": keyfile or None,
792
+ "cert_reqs": (getattr(ssl, "CERT_REQUIRED", 2) if (verify and ca_file) else getattr(ssl, "CERT_NONE", 0)),
793
+ }
794
+ if verify and ca_file:
795
+ kwargs["ca_certs"] = ca_file
796
+ return ssl.wrap_socket(sock, **kwargs)
797
+
798
+ # ---------- IPv6 / multi-A dialer + keepalive ----------
799
+ def _enable_keepalive(s, idle=60, intvl=15, cnt=4):
800
+ try:
801
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
802
+ if hasattr(socket, 'TCP_KEEPIDLE'):
803
+ s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, idle)
804
+ if hasattr(socket, 'TCP_KEEPINTVL'):
805
+ s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl)
806
+ if hasattr(socket, 'TCP_KEEPCNT'):
807
+ s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt)
808
+ except Exception:
809
+ pass
810
+
811
+ def _connect_stream(host, port, timeout):
812
+ err = None
813
+ for fam, st, proto, _, sa in socket.getaddrinfo(host, int(port), 0, socket.SOCK_STREAM):
814
+ try:
815
+ s = socket.socket(fam, st, proto)
816
+ if timeout is not None:
817
+ s.settimeout(timeout)
818
+ try: s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
819
+ except Exception: pass
820
+ s.connect(sa)
821
+ _enable_keepalive(s)
822
+ return s
823
+ except Exception as e:
824
+ err = e
825
+ try: s.close()
826
+ except Exception: pass
827
+ if err: raise err
828
+ raise RuntimeError("no usable address")
829
+
830
+ # ---------- Auth: AF1 (HMAC) + legacy fallback ----------
831
+ # AF1: single ASCII line ending with '\n':
832
+ # AF1 ts=<unix> user=<b64url> nonce=<b64url_12B> scope=<b64url> alg=sha256 mac=<hex>\n
833
+ def _b64url_encode(b):
834
+ s = base64.urlsafe_b64encode(b)
835
+ return _to_text(s.rstrip(b'='))
836
+
837
+ def _b64url_decode(s):
838
+ s = _to_bytes(s)
839
+ pad = b'=' * ((4 - (len(s) % 4)) % 4)
840
+ return base64.urlsafe_b64decode(s + pad)
841
+
842
+ def _auth_msg(ts_int, user_utf8, nonce_bytes, scope_utf8, length_str, sha_hex):
843
+ # canonical message for MAC: v1|ts|user|nonce_b64|scope|len|sha
844
+ return _to_bytes("v1|%d|%s|%s|%s|%s|%s" % (
845
+ ts_int,
846
+ _to_text(user_utf8),
847
+ _b64url_encode(nonce_bytes),
848
+ _to_text(scope_utf8),
849
+ length_str if length_str is not None else "",
850
+ sha_hex if sha_hex is not None else "",
851
+ ))
852
+
853
+ def build_auth_blob_v1(user, secret, scope=u"", now=None, length=None, sha_hex=None):
854
+ """
855
+ user: text; secret: text/bytes (HMAC key)
856
+ scope: optional text (e.g., path)
857
+ length: int or None (payload bytes)
858
+ sha_hex: ascii hex SHA-256 of payload (optional)
859
+ """
860
+ ts = int(time.time() if now is None else now)
861
+ user_b = _to_bytes(user or u"")
862
+ scope_b = _to_bytes(scope or u"")
863
+ key_b = _to_bytes(secret or u"")
864
+ nonce = os.urandom(12)
865
+
866
+ length_str = (str(int(length)) if (length is not None and int(length) >= 0) else "")
867
+ sha_hex = (sha_hex or None)
868
+ mac = hmac.new(
869
+ key_b,
870
+ _auth_msg(ts, user_b, nonce, scope_b, length_str, sha_hex),
871
+ hashlib.sha256
872
+ ).hexdigest()
873
+
874
+ line = "AF1 ts=%d user=%s nonce=%s scope=%s len=%s sha=%s alg=sha256 mac=%s\n" % (
875
+ ts,
876
+ _b64url_encode(user_b),
877
+ _b64url_encode(nonce),
878
+ _b64url_encode(scope_b),
879
+ length_str,
880
+ (sha_hex or ""),
881
+ mac,
882
+ )
883
+ return _to_bytes(line)
884
+
885
+ from collections import deque
886
+ class _NonceCache(object):
887
+ def __init__(self, max_items=10000, ttl_seconds=600):
888
+ self.max_items = int(max_items); self.ttl = int(ttl_seconds)
889
+ self.q = deque(); self.s = set()
890
+ def seen(self, nonce_b64, now_ts):
891
+ # evict old / over-capacity
892
+ while self.q and (now_ts - self.q[0][0] > self.ttl or len(self.q) > self.max_items):
893
+ _, n = self.q.popleft(); self.s.discard(n)
894
+ if nonce_b64 in self.s: return True
895
+ self.s.add(nonce_b64); self.q.append((now_ts, nonce_b64))
896
+ return False
897
+
898
+ _NONCES = _NonceCache()
899
+
900
+ def verify_auth_blob_v1(blob_bytes, expected_user=None, secret=None,
901
+ max_skew=600, expect_scope=None):
902
+ """
903
+ Returns (ok_bool, user_text, scope_text, reason_text, length_or_None, sha_hex_or_None)
904
+ """
905
+ try:
906
+ line = _to_text(blob_bytes).strip()
907
+ if not line.startswith("AF1 "):
908
+ return (False, None, None, "bad magic", None, None)
909
+ kv = {}
910
+ for tok in line.split()[1:]:
911
+ if '=' in tok:
912
+ k, v = tok.split('=', 1); kv[k] = v
913
+
914
+ for req in ("ts","user","nonce","mac","alg"):
915
+ if req not in kv:
916
+ return (False, None, None, "missing %s" % req, None, None)
917
+ if kv["alg"].lower() != "sha256":
918
+ return (False, None, None, "alg", None, None)
919
+
920
+ ts = int(kv["ts"])
921
+ userb = _b64url_decode(kv["user"])
922
+ nonce_b64 = kv["nonce"]; nonce = _b64url_decode(nonce_b64)
923
+ scopeb = _b64url_decode(kv.get("scope","")) if kv.get("scope") else b""
924
+ length_str = kv.get("len","")
925
+ sha_hex = kv.get("sha","") or None
926
+ mac = kv["mac"]
927
+
928
+ now = int(time.time())
929
+ if abs(now - ts) > int(max_skew):
930
+ return (False, None, None, "skew", None, None)
931
+
932
+ if _NONCES.seen(nonce_b64, now):
933
+ return (False, None, None, "replay", None, None)
934
+
935
+ if expected_user is not None and _to_bytes(expected_user) != userb:
936
+ return (False, None, None, "user", None, None)
937
+
938
+ calc = hmac.new(
939
+ _to_bytes(secret or u""),
940
+ _auth_msg(ts, userb, nonce, scopeb, length_str, sha_hex),
941
+ hashlib.sha256
942
+ ).hexdigest()
943
+ if not hmac.compare_digest(calc, mac):
944
+ return (False, None, None, "mac", None, None)
945
+
946
+ if expect_scope is not None and _to_bytes(expect_scope) != scopeb:
947
+ return (False, None, None, "scope", None, None)
948
+
949
+ length = int(length_str) if (length_str and length_str.isdigit()) else None
950
+ return (True, _to_text(userb), _to_text(scopeb), "ok", length, sha_hex)
951
+ except Exception as e:
952
+ return (False, None, None, "exc:%s" % e, None, None)
953
+
954
+ # Legacy blob (kept for backward compatibility)
955
+ _MAGIC = b"AUTH\0"; _OK = b"OK"; _NO = b"NO"
956
+
957
+ def _build_auth_blob_legacy(user, pw):
958
+ return _MAGIC + _to_bytes(user) + b"\0" + _to_bytes(pw) + b"\0"
959
+
960
+ def _parse_auth_blob_legacy(data):
961
+ if not data.startswith(_MAGIC):
962
+ return (None, None)
963
+ rest = data[len(_MAGIC):]
964
+ try:
965
+ user, rest = rest.split(b"\0", 1)
966
+ pw, _tail = rest.split(b"\0", 1)
967
+ return (user, pw)
968
+ except Exception:
969
+ return (None, None)
970
+
971
+ # ---------- URL helpers ----------
972
+ def _qflag(qs, key, default=False):
973
+ v = qs.get(key, [None])[0]
974
+ if v is None: return bool(default)
975
+ return _to_text(v).lower() in ("1", "true", "yes", "on")
976
+
977
+ def _qnum(qs, key, default=None, cast=float):
978
+ v = qs.get(key, [None])[0]
979
+ if v is None or v == "": return default
980
+ try: return cast(v)
981
+ except Exception: return default
982
+
983
+ def _qstr(qs, key, default=None):
984
+ v = qs.get(key, [None])[0]
985
+ if v is None: return default
986
+ return v
987
+
988
+ def _parse_net_url(url):
989
+ """
990
+ Parse tcp:// / udp:// URL and extract transport options.
991
+ Returns (parts, opts)
992
+ """
993
+ parts = urlparse(url)
994
+ qs = parse_qs(parts.query or "")
995
+
996
+ proto = parts.scheme.lower()
997
+ if proto not in ("tcp", "udp"):
998
+ raise ValueError("Only tcp:// or udp:// supported here")
999
+
1000
+ user = unquote(parts.username) if parts.username else None
1001
+ pw = unquote(parts.password) if parts.password else None
1002
+
1003
+ use_ssl = _qflag(qs, "ssl", False) if proto == "tcp" else False
1004
+ ssl_verify = _qflag(qs, "verify", True)
1005
+ ssl_ca_file = _qstr(qs, "ca", None)
1006
+ ssl_cert = _qstr(qs, "cert", None)
1007
+ ssl_key = _qstr(qs, "key", None)
1008
+
1009
+ timeout = _qnum(qs, "timeout", None, float)
1010
+ total_timeout = _qnum(qs, "total_timeout", None, float)
1011
+ chunk_size = int(_qnum(qs, "chunk", 65536, float))
1012
+
1013
+ force_auth = _qflag(qs, "auth", False)
1014
+ want_sha = _qflag(qs, "sha", True) # <— NEW: default compute sha
1015
+
1016
+ opts = dict(
1017
+ proto=proto,
1018
+ host=parts.hostname or "127.0.0.1",
1019
+ port=int(parts.port or 0),
1020
+
1021
+ user=user, pw=pw, force_auth=force_auth,
1022
+
1023
+ use_ssl=use_ssl, ssl_verify=ssl_verify,
1024
+ ssl_ca_file=ssl_ca_file, ssl_certfile=ssl_cert, ssl_keyfile=ssl_key,
1025
+
1026
+ timeout=timeout, total_timeout=total_timeout, chunk_size=chunk_size,
1027
+
1028
+ server_hostname=parts.hostname or None,
1029
+
1030
+ # new option
1031
+ want_sha=want_sha,
1032
+
1033
+ # convenience (used as scope in AF1)
1034
+ path=(parts.path or u""),
1035
+ )
1036
+ return parts, opts
1037
+
1038
+ def _rewrite_url_without_auth(url):
1039
+ u = urlparse(url)
1040
+ netloc = u.hostname or ''
1041
+ if u.port:
1042
+ netloc += ':' + str(u.port)
1043
+ rebuilt = urlunparse((u.scheme, netloc, u.path, u.params, u.query, u.fragment))
1044
+ usr = unquote(u.username) if u.username else ''
1045
+ pwd = unquote(u.password) if u.password else ''
1046
+ return rebuilt, usr, pwd
1047
+
1048
+ def _guess_filename(url, filename):
1049
+ if filename:
1050
+ return filename
1051
+ path = urlparse(url).path or ''
1052
+ base = os.path.basename(path)
1053
+ return base or 'OutFile.'+__file_format_extension__
1054
+
1055
+ # ---- progress + rate limiting helpers ----
1056
+ try:
1057
+ monotonic = time.monotonic # Py3
1058
+ except Exception:
1059
+ # Py2 fallback: time.time() is good enough for coarse throttling
1060
+ monotonic = time.time
1061
+
1062
+ def _progress_tick(now_bytes, total_bytes, last_ts, last_bytes, rate_limit_bps, min_interval=0.1):
1063
+ """
1064
+ Returns (sleep_seconds, new_last_ts, new_last_bytes).
1065
+ - If rate_limit_bps is set, computes how long to sleep to keep average <= limit.
1066
+ - Also enforces a minimum interval between progress callbacks (handled by caller).
1067
+ """
1068
+ now = monotonic()
1069
+ elapsed = max(1e-9, now - last_ts)
1070
+ # Desired time to have elapsed for the given rate:
1071
+ desired = (now_bytes - last_bytes) / float(rate_limit_bps) if rate_limit_bps else 0.0
1072
+ extra = desired - elapsed
1073
+ return (max(0.0, extra), now, now_bytes)
1074
+
1075
+ def _discover_len_and_reset(fobj):
1076
+ """
1077
+ Try hard to get total length and restore original position.
1078
+ Returns (length_or_None, start_pos_or_None).
1079
+ Works with seekable files and BytesIO; leaves stream position unchanged.
1080
+ """
1081
+ # Generic seek/tell
1082
+ try:
1083
+ pos0 = fobj.tell()
1084
+ fobj.seek(0, os.SEEK_END)
1085
+ end = fobj.tell()
1086
+ fobj.seek(pos0, os.SEEK_SET)
1087
+ if end is not None and pos0 is not None and end >= pos0:
1088
+ return (end - pos0, pos0)
1089
+ except Exception:
1090
+ pass
1091
+
1092
+ # BytesIO fast path
1093
+ try:
1094
+ getvalue = getattr(fobj, "getvalue", None)
1095
+ if callable(getvalue):
1096
+ buf = getvalue()
1097
+ L = len(buf)
1098
+ try:
1099
+ pos0 = fobj.tell()
1100
+ except Exception:
1101
+ pos0 = 0
1102
+ return (max(0, L - pos0), pos0)
1103
+ except Exception:
1104
+ pass
1105
+
1106
+ # Memoryview/getbuffer
1107
+ try:
1108
+ getbuffer = getattr(fobj, "getbuffer", None)
1109
+ if callable(getbuffer):
1110
+ mv = getbuffer()
1111
+ L = len(mv)
1112
+ try:
1113
+ pos0 = fobj.tell()
1114
+ except Exception:
1115
+ pos0 = 0
1116
+ return (max(0, L - pos0), pos0)
1117
+ except Exception:
1118
+ pass
1119
+
1120
+ return (None, None)
1121
+
1122
+
661
1123
  def DetectTarBombCatFileArray(listarrayfiles,
662
1124
  top_file_ratio_threshold=0.6,
663
1125
  min_members_for_ratio=4,
@@ -2782,6 +3244,7 @@ def ReadFileDataWithContent(fp, filestart=0, listonly=False, uncompress=True, sk
2782
3244
  break
2783
3245
  flist.append(HeaderOut)
2784
3246
  countnum = countnum + 1
3247
+ outlist.update({'fp': fp})
2785
3248
  return flist
2786
3249
 
2787
3250
 
@@ -2848,7 +3311,7 @@ def ReadFileDataWithContentToArray(fp, filestart=0, seekstart=0, seekend=0, list
2848
3311
  return False
2849
3312
  formversions = re.search('(.*?)(\\d+)', formstring).groups()
2850
3313
  fcompresstype = ""
2851
- outlist = {'fnumfiles': fnumfiles, 'fformat': formversions[0], 'fcompression': fcompresstype, 'fencoding': fhencoding, 'fversion': formversions[1], 'fostype': fostype, 'fheadersize': fheadsize, 'fsize': CatSizeEnd, 'fnumfields': fnumfields + 2, 'fformatspecs': formatspecs, 'fchecksumtype': fprechecksumtype, 'fheaderchecksum': fprechecksum, 'frawheader': [formstring] + inheader, 'fextrafields': fnumextrafields, 'fextrafieldsize': fnumextrafieldsize, 'fextradata': fextrafieldslist, 'ffilelist': []}
3314
+ outlist = {'fnumfiles': fnumfiles, 'ffilestart': filestart, 'fformat': formversions[0], 'fcompression': fcompresstype, 'fencoding': fhencoding, 'fversion': formversions[1], 'fostype': fostype, 'fheadersize': fheadsize, 'fsize': CatSizeEnd, 'fnumfields': fnumfields + 2, 'fformatspecs': formatspecs, 'fchecksumtype': fprechecksumtype, 'fheaderchecksum': fprechecksum, 'frawheader': [formstring] + inheader, 'fextrafields': fnumextrafields, 'fextrafieldsize': fnumextrafieldsize, 'fextradata': fextrafieldslist, 'ffilelist': []}
2852
3315
  if (seekstart < 0) or (seekstart > fnumfiles):
2853
3316
  seekstart = 0
2854
3317
  if (seekend == 0) or (seekend > fnumfiles) or (seekend < seekstart):
@@ -3291,6 +3754,41 @@ def ReadInMultipleFilesWithContentToArray(infile, fmttype="auto", filestart=0, s
3291
3754
  return ReadInMultipleFileWithContentToArray(infile, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend)
3292
3755
 
3293
3756
 
3757
+ def ReadInStackedFileWithContentToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False):
3758
+ outretval = []
3759
+ outstartfile = filestart
3760
+ outfsize = float('inf')
3761
+ while True:
3762
+ if outstartfile >= outfsize: # stop when function signals False
3763
+ break
3764
+ outarray = CatFileToArray(infile, fmttype, outstartfile, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, True)
3765
+ outfsize = outarray['fsize']
3766
+ if outarray is False: # stop when function signals False
3767
+ break
3768
+ infile = outarray['fp']
3769
+ outstartfile = infile.tell()
3770
+ outretval.append(outarray)
3771
+ return outretval
3772
+
3773
+
3774
+ def ReadInStackedFilesWithContentToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False):
3775
+ return ReadInStackedFileWithContentToArray(infile, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend)
3776
+
3777
+
3778
+ def ReadInMultipleStackedFileWithContentToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False):
3779
+ if(isinstance(infile, (list, tuple, ))):
3780
+ pass
3781
+ else:
3782
+ infile = [infile]
3783
+ outretval = {}
3784
+ for curfname in infile:
3785
+ outretval[curfname] = ReadInStackedFileWithContentToArray(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend)
3786
+ return outretval
3787
+
3788
+ def ReadInMultipleStackedFilesWithContentToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False):
3789
+ return ReadInMultipleStackedFileWithContentToArray(infile, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend)
3790
+
3791
+
3294
3792
  def ReadInFileWithContentToList(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False):
3295
3793
  if(IsNestedDict(formatspecs) and fmttype!="auto" and fmttype in formatspecs):
3296
3794
  formatspecs = formatspecs[fmttype]
@@ -3472,7 +3970,7 @@ def ReadInMultipleFileWithContentToList(infile, fmttype="auto", filestart=0, see
3472
3970
  infile = [infile]
3473
3971
  outretval = {}
3474
3972
  for curfname in infile:
3475
- curretfile[curfname] = ReadInFileWithContentToList(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend)
3973
+ outretval[curfname] = ReadInFileWithContentToList(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend)
3476
3974
  return outretval
3477
3975
 
3478
3976
  def ReadInMultipleFilesWithContentToList(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False):
@@ -4041,12 +4539,6 @@ def AppendFilesWithContent(infiles, fp, dirlistfromtxt=False, filevalues=[], ext
4041
4539
  fcsize, fuid, funame, fgid, fgname, fcurfid, fcurinode, flinkcount, fdev, fdev_minor, fdev_major, "+"+str(len(formatspecs['format_delimiter']))]
4042
4540
  AppendFileHeaderWithContent(
4043
4541
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
4044
- if(numfiles > 0):
4045
- try:
4046
- fp.write(AppendNullBytes(
4047
- ["0", "0"], formatspecs['format_delimiter']))
4048
- except OSError:
4049
- return False
4050
4542
  fp.seek(0, 0)
4051
4543
  return fp
4052
4544
 
@@ -4110,12 +4602,6 @@ def AppendListsWithContent(inlist, fp, dirlistfromtxt=False, filevalues=[], extr
4110
4602
  fcontents.seek(0, 0)
4111
4603
  AppendFileHeaderWithContent(
4112
4604
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
4113
- if(numfiles > 0):
4114
- try:
4115
- fp.write(AppendNullBytes(
4116
- ["0", "0"], formatspecs['format_delimiter']))
4117
- except OSError:
4118
- return False
4119
4605
  return fp
4120
4606
 
4121
4607
 
@@ -5508,12 +5994,6 @@ def PackCatFile(infiles, outfile, dirlistfromtxt=False, fmttype="auto", compress
5508
5994
  AppendFileHeaderWithContent(
5509
5995
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
5510
5996
  fcontents.close()
5511
- if(numfiles > 0):
5512
- try:
5513
- fp.write(AppendNullBytes(
5514
- ["0", "0"], formatspecs['format_delimiter']))
5515
- except OSError:
5516
- return False
5517
5997
  if(outfile == "-" or outfile is None or hasattr(outfile, "read") or hasattr(outfile, "write")):
5518
5998
  fp = CompressOpenFileAlt(
5519
5999
  fp, compression, compressionlevel, compressionuselist, formatspecs)
@@ -5809,12 +6289,6 @@ def PackCatFileFromTarFile(infile, outfile, fmttype="auto", compression="auto",
5809
6289
  AppendFileHeaderWithContent(
5810
6290
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
5811
6291
  fcontents.close()
5812
- if(numfiles > 0):
5813
- try:
5814
- fp.write(AppendNullBytes(
5815
- ["0", "0"], formatspecs['format_delimiter']))
5816
- except OSError:
5817
- return False
5818
6292
  if(outfile == "-" or outfile is None or hasattr(outfile, "read") or hasattr(outfile, "write")):
5819
6293
  fp = CompressOpenFileAlt(
5820
6294
  fp, compression, compressionlevel, compressionuselist, formatspecs)
@@ -6103,12 +6577,6 @@ def PackCatFileFromZipFile(infile, outfile, fmttype="auto", compression="auto",
6103
6577
  AppendFileHeaderWithContent(
6104
6578
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
6105
6579
  fcontents.close()
6106
- if(numfiles > 0):
6107
- try:
6108
- fp.write(AppendNullBytes(
6109
- ["0", "0"], formatspecs['format_delimiter']))
6110
- except OSError:
6111
- return False
6112
6580
  if(outfile == "-" or outfile is None or hasattr(outfile, "read") or hasattr(outfile, "write")):
6113
6581
  fp = CompressOpenFileAlt(
6114
6582
  fp, compression, compressionlevel, compressionuselist, formatspecs)
@@ -6423,12 +6891,6 @@ if(rarfile_support):
6423
6891
  AppendFileHeaderWithContent(
6424
6892
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
6425
6893
  fcontents.close()
6426
- if(numfiles > 0):
6427
- try:
6428
- fp.write(AppendNullBytes(
6429
- ["0", "0"], formatspecs['format_delimiter']))
6430
- except OSError:
6431
- return False
6432
6894
  if(outfile == "-" or outfile is None or hasattr(outfile, "read") or hasattr(outfile, "write")):
6433
6895
  fp = CompressOpenFileAlt(
6434
6896
  fp, compression, compressionlevel, compressionuselist, formatspecs)
@@ -6677,12 +7139,6 @@ if(py7zr_support):
6677
7139
  AppendFileHeaderWithContent(
6678
7140
  fp, tmpoutlist, extradata, jsondata, fcontents.read(), [checksumtype[1], checksumtype[2], checksumtype[3]], formatspecs)
6679
7141
  fcontents.close()
6680
- if(numfiles > 0):
6681
- try:
6682
- fp.write(AppendNullBytes(
6683
- ["0", "0"], formatspecs['format_delimiter']))
6684
- except OSError:
6685
- return False
6686
7142
  if(outfile == "-" or outfile is None or hasattr(outfile, "read") or hasattr(outfile, "write")):
6687
7143
  fp = CompressOpenFileAlt(
6688
7144
  fp, compression, compressionlevel, compressionuselist, formatspecs)
@@ -7072,24 +7528,73 @@ def CatFileValidate(infile, fmttype="auto", filestart=0, formatspecs=__file_form
7072
7528
  return False
7073
7529
 
7074
7530
 
7075
- def CatFileValidateFile(infile, fmttype="auto", formatspecs=__file_format_multi_dict__, verbose=False, returnfp=False):
7076
- return CatFileValidate(infile, fmttype, formatspecs, verbose, returnfp)
7531
+ def CatFileValidateFile(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7532
+ return CatFileValidate(infile, fmttype, filestart, formatspecs, seektoend, verbose, returnfp)
7533
+
7534
+
7535
+ def CatFileValidateMultiple(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7536
+ if(isinstance(infile, (list, tuple, ))):
7537
+ pass
7538
+ else:
7539
+ infile = [infile]
7540
+ outretval = True
7541
+ for curfname in infile:
7542
+ curretfile = CatFileValidate(curfname, fmttype, filestart, formatspecs, seektoend, verbose, returnfp)
7543
+ if(not curretfile):
7544
+ outretval = False
7545
+ return outretval
7546
+
7547
+ def CatFileValidateMultipleFiles(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7548
+ return CatFileValidateMultiple(infile, fmttype, filestart, formatspecs, seektoend, verbose, returnfp)
7549
+
7550
+
7551
+ def StackedCatFileValidate(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7552
+ outretval = []
7553
+ outstartfile = filestart
7554
+ outfsize = float('inf')
7555
+ while True:
7556
+ if outstartfile >= outfsize: # stop when function signals False
7557
+ break
7558
+ is_valid_file = CatFileValidate(infile, fmttype, filestart, formatspecs, seektoend, verbose, True)
7559
+ if is_valid_file is False: # stop when function signals False
7560
+ outretval.append(is_valid_file)
7561
+ else:
7562
+ outretval.append(True)
7563
+ infile = is_valid_file
7564
+ outstartfile = infile.tell()
7565
+ try:
7566
+ infile.seek(0, 2)
7567
+ except OSError:
7568
+ SeekToEndOfFile(infile)
7569
+ except ValueError:
7570
+ SeekToEndOfFile(infile)
7571
+ outfsize = infile.tell()
7572
+ infile.seek(outstartfile, 0)
7573
+ if(returnfp):
7574
+ return infile
7575
+ else:
7576
+ infile.close()
7577
+ return outretval
7578
+
7077
7579
 
7580
+ def StackedCatFileValidateFile(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7581
+ return StackedCatFileValidate(infile, fmttype, filestart, formatspecs, seektoend, verbose, returnfp)
7078
7582
 
7079
- def CatFileValidateMultiple(infile, fmttype="auto", formatspecs=__file_format_multi_dict__, verbose=False, returnfp=False):
7583
+
7584
+ def StackedCatFileValidateMultiple(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7080
7585
  if(isinstance(infile, (list, tuple, ))):
7081
7586
  pass
7082
7587
  else:
7083
7588
  infile = [infile]
7084
7589
  outretval = True
7085
7590
  for curfname in infile:
7086
- curretfile = CatFileValidate(curfname, fmttype, formatspecs, verbose, returnfp)
7591
+ curretfile = StackedCatFileValidate(curfname, fmttype, filestart, formatspecs, seektoend, verbose, returnfp)
7087
7592
  if(not curretfile):
7088
7593
  outretval = False
7089
7594
  return outretval
7090
7595
 
7091
- def CatFileValidateMultipleFiles(infile, fmttype="auto", formatspecs=__file_format_multi_dict__, verbose=False, returnfp=False):
7092
- return CatFileValidateMultiple(infile, fmttype, formatspecs, verbose, returnfp)
7596
+ def StackedCatFileValidateMultipleFiles(infile, fmttype="auto", filestart=0, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
7597
+ return StackedCatFileValidateMultiple(infile, fmttype, filestart, formatspecs, seektoend, verbose, returnfp)
7093
7598
 
7094
7599
  def CatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
7095
7600
  if(IsNestedDict(formatspecs) and fmttype!="auto" and fmttype in formatspecs):
@@ -7102,20 +7607,20 @@ def CatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0,
7102
7607
  fp = infile
7103
7608
  fp.seek(filestart, 0)
7104
7609
  fp = UncompressFileAlt(fp, formatspecs, filestart)
7105
- checkcompressfile = CheckCompressionSubType(fp, formatspecs, filestart, True)
7106
- if(IsNestedDict(formatspecs) and checkcompressfile in formatspecs):
7107
- formatspecs = formatspecs[checkcompressfile]
7108
- if(checkcompressfile == "tarfile" and TarFileCheck(infile)):
7610
+ compresscheck = CheckCompressionSubType(fp, formatspecs, filestart, True)
7611
+ if(IsNestedDict(formatspecs) and compresscheck in formatspecs):
7612
+ formatspecs = formatspecs[compresscheck]
7613
+ if(compresscheck == "tarfile" and TarFileCheck(infile)):
7109
7614
  return TarFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7110
- elif(checkcompressfile == "zipfile" and zipfile.is_zipfile(infile)):
7615
+ elif(compresscheck == "zipfile" and zipfile.is_zipfile(infile)):
7111
7616
  return ZipFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7112
- elif(rarfile_support and checkcompressfile == "rarfile" and (rarfile.is_rarfile(infile) or rarfile.is_rarfile_sfx(infile))):
7617
+ elif(rarfile_support and compresscheck == "rarfile" and (rarfile.is_rarfile(infile) or rarfile.is_rarfile_sfx(infile))):
7113
7618
  return RarFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7114
- elif(py7zr_support and checkcompressfile == "7zipfile" and py7zr.is_7zfile(infile)):
7619
+ elif(py7zr_support and compresscheck == "7zipfile" and py7zr.is_7zfile(infile)):
7115
7620
  return SevenZipFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7116
- elif(IsSingleDict(formatspecs) and checkcompressfile != formatspecs['format_magic']):
7621
+ elif(IsSingleDict(formatspecs) and compresscheck != formatspecs['format_magic']):
7117
7622
  return False
7118
- elif(IsNestedDict(formatspecs) and checkcompressfile not in formatspecs):
7623
+ elif(IsNestedDict(formatspecs) and compresscheck not in formatspecs):
7119
7624
  return False
7120
7625
  if(not fp):
7121
7626
  return False
@@ -7128,9 +7633,9 @@ def CatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0,
7128
7633
  shutil.copyfileobj(sys.stdin, fp)
7129
7634
  fp.seek(filestart, 0)
7130
7635
  fp = UncompressFileAlt(fp, formatspecs, filestart)
7131
- checkcompressfile = CheckCompressionSubType(fp, formatspecs, filestart, True)
7132
- if(IsNestedDict(formatspecs) and checkcompressfile in formatspecs):
7133
- formatspecs = formatspecs[checkcompressfile]
7636
+ compresscheck = CheckCompressionSubType(fp, formatspecs, filestart, True)
7637
+ if(IsNestedDict(formatspecs) and compresscheck in formatspecs):
7638
+ formatspecs = formatspecs[compresscheck]
7134
7639
  if(not fp):
7135
7640
  return False
7136
7641
  fp.seek(filestart, 0)
@@ -7157,20 +7662,20 @@ def CatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0,
7157
7662
  fp.seek(filestart, 0)
7158
7663
  else:
7159
7664
  infile = RemoveWindowsPath(infile)
7160
- checkcompressfile = CheckCompressionSubType(infile, formatspecs, filestart, True)
7161
- if(IsNestedDict(formatspecs) and checkcompressfile in formatspecs):
7162
- formatspecs = formatspecs[checkcompressfile]
7163
- if(checkcompressfile == "tarfile" and TarFileCheck(infile)):
7665
+ compresscheck = CheckCompressionSubType(infile, formatspecs, filestart, True)
7666
+ if(IsNestedDict(formatspecs) and compresscheck in formatspecs):
7667
+ formatspecs = formatspecs[compresscheck]
7668
+ if(compresscheck == "tarfile" and TarFileCheck(infile)):
7164
7669
  return TarFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7165
- elif(checkcompressfile == "zipfile" and zipfile.is_zipfile(infile)):
7670
+ elif(compresscheck == "zipfile" and zipfile.is_zipfile(infile)):
7166
7671
  return ZipFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7167
- elif(rarfile_support and checkcompressfile == "rarfile" and (rarfile.is_rarfile(infile) or rarfile.is_rarfile_sfx(infile))):
7672
+ elif(rarfile_support and compresscheck == "rarfile" and (rarfile.is_rarfile(infile) or rarfile.is_rarfile_sfx(infile))):
7168
7673
  return RarFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7169
- elif(py7zr_support and checkcompressfile == "7zipfile" and py7zr.is_7zfile(infile)):
7674
+ elif(py7zr_support and compresscheck == "7zipfile" and py7zr.is_7zfile(infile)):
7170
7675
  return SevenZipFileToArray(infile, 0, 0, listonly, contentasfile, skipchecksum, formatspecs, seektoend, returnfp)
7171
- elif(IsSingleDict(formatspecs) and checkcompressfile != formatspecs['format_magic']):
7676
+ elif(IsSingleDict(formatspecs) and compresscheck != formatspecs['format_magic']):
7172
7677
  return False
7173
- elif(IsNestedDict(formatspecs) and checkcompressfile not in formatspecs):
7678
+ elif(IsNestedDict(formatspecs) and compresscheck not in formatspecs):
7174
7679
  return False
7175
7680
  compresscheck = CheckCompressionType(infile, formatspecs, filestart, True)
7176
7681
  if(not compresscheck):
@@ -7263,7 +7768,7 @@ def CatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0,
7263
7768
  fcompresstype = compresscheck
7264
7769
  if(fcompresstype==formatspecs['format_magic']):
7265
7770
  fcompresstype = ""
7266
- outlist = {'fnumfiles': fnumfiles, 'fformat': formversions[0], 'fcompression': fcompresstype, 'fencoding': fhencoding, 'fversion': formversions[1], 'fostype': fostype, 'fheadersize': fheadsize, 'fsize': CatSizeEnd, 'fnumfields': fnumfields + 2, 'fformatspecs': formatspecs, 'fchecksumtype': fprechecksumtype, 'fheaderchecksum': fprechecksum, 'frawheader': [formstring] + inheader, 'fextrafields': fnumextrafields, 'fextrafieldsize': fnumextrafieldsize, 'fextradata': fextrafieldslist, 'ffilelist': []}
7771
+ outlist = {'fnumfiles': fnumfiles, 'ffilestart': filestart, 'fformat': formversions[0], 'fcompression': fcompresstype, 'fencoding': fhencoding, 'fversion': formversions[1], 'fostype': fostype, 'fheadersize': fheadsize, 'fsize': CatSizeEnd, 'fnumfields': fnumfields + 2, 'fformatspecs': formatspecs, 'fchecksumtype': fprechecksumtype, 'fheaderchecksum': fprechecksum, 'frawheader': [formstring] + inheader, 'fextrafields': fnumextrafields, 'fextrafieldsize': fnumextrafieldsize, 'fextradata': fextrafieldslist, 'ffilelist': []}
7267
7772
  if (seekstart < 0) or (seekstart > fnumfiles):
7268
7773
  seekstart = 0
7269
7774
  if (seekend == 0) or (seekend > fnumfiles) or (seekend < seekstart):
@@ -7549,6 +8054,7 @@ def CatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0,
7549
8054
  outlist.update({'fp': fp})
7550
8055
  else:
7551
8056
  fp.close()
8057
+ outlist.update({'fp': None})
7552
8058
  return outlist
7553
8059
 
7554
8060
 
@@ -7559,13 +8065,48 @@ def MultipleCatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, see
7559
8065
  infile = [infile]
7560
8066
  outretval = {}
7561
8067
  for curfname in infile:
7562
- curretfile[curfname] = CatFileToArray(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, returnfp)
8068
+ outretval[curfname] = CatFileToArray(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, returnfp)
7563
8069
  return outretval
7564
8070
 
7565
8071
  def MultipleCatFilesToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
7566
8072
  return MultipleCatFileToArray(infile, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, returnfp)
7567
8073
 
7568
8074
 
8075
+ def StackedCatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
8076
+ outretval = []
8077
+ outstartfile = filestart
8078
+ outfsize = float('inf')
8079
+ while True:
8080
+ if outstartfile >= outfsize: # stop when function signals False
8081
+ break
8082
+ outarray = CatFileToArray(infile, fmttype, outstartfile, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, True)
8083
+ outfsize = outarray['fsize']
8084
+ if outarray is False: # stop when function signals False
8085
+ break
8086
+ infile = outarray['fp']
8087
+ outstartfile = infile.tell()
8088
+ if(not returnfp):
8089
+ outarray.update({"fp": None})
8090
+ outretval.append(outarray)
8091
+ if(not returnfp):
8092
+ infile.close()
8093
+ return outretval
8094
+
8095
+
8096
+ def MultipleStackedCatFileToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
8097
+ if(isinstance(infile, (list, tuple, ))):
8098
+ pass
8099
+ else:
8100
+ infile = [infile]
8101
+ outretval = {}
8102
+ for curfname in infile:
8103
+ outretval[curfname] = StackedCatFileToArray(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, returnfp)
8104
+ return outretval
8105
+
8106
+ def MultipleStackedCatFilesToArray(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
8107
+ return MultipleStackedCatFileToArray(infile, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, returnfp)
8108
+
8109
+
7569
8110
  def CatFileStringToArray(instr, filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
7570
8111
  checkcompressfile = CheckCompressionSubType(infile, formatspecs, filestart, True)
7571
8112
  if(IsNestedDict(formatspecs) and checkcompressfile in formatspecs):
@@ -7651,74 +8192,126 @@ def ListDirToArray(infiles, dirlistfromtxt=False, fmttype=__file_format_default_
7651
8192
  outarray = MkTempFile()
7652
8193
  packform = PackCatFile(infiles, outarray, dirlistfromtxt, fmttype, compression, compresswholefile,
7653
8194
  compressionlevel, followlink, checksumtype, extradata, formatspecs, verbose, True)
7654
- listarrayfiles = CatFileToArray(outarray, "auto", filestart, seekstart, seekend, listonly, True, skipchecksum, formatspecs, seektoend, returnfp)
8195
+ listarrayfiles = CatFileToArray(outarray, "auto", filestart, seekstart, seekend, listonly, True, True, skipchecksum, formatspecs, seektoend, returnfp)
7655
8196
  return listarrayfiles
7656
8197
 
7657
8198
 
8199
+ # ===== Function (keeps inarray schema; returns entries + indexes) =====
8200
+
7658
8201
  def CatFileArrayToArrayIndex(inarray, returnfp=False):
7659
- if(isinstance(inarray, dict)):
7660
- listarrayfiles = inarray
7661
- else:
8202
+ """
8203
+ Build a bidirectional index over an archive listing while preserving the
8204
+ input 'inarray' as-is. Python 2/3 compatible, no external deps.
8205
+
8206
+ Input (unchanged contract):
8207
+ inarray: dict with at least:
8208
+ - 'ffilelist': list of dicts: {'fname': <str>, 'fid': <any>, 'ftype': <int>}
8209
+ - 'fnumfiles': int (expected count)
8210
+ - optional 'fp': any (passed through if returnfp=True)
8211
+
8212
+ Output structure:
8213
+ {
8214
+ 'list': inarray, # alias to original input (not copied)
8215
+ 'fp': inarray.get('fp') or None,
8216
+ 'entries': { fid: {'name': fname, 'type': ftype} },
8217
+ 'indexes': {
8218
+ 'by_name': { fname: fid },
8219
+ 'by_type': {
8220
+ <category>: {
8221
+ 'by_name': { fname: fid },
8222
+ 'by_id': { fid: fname },
8223
+ 'count': <int>
8224
+ }, ...
8225
+ }
8226
+ },
8227
+ 'counts': {
8228
+ 'total': <int>,
8229
+ 'by_type': { <category>: <int>, ... }
8230
+ },
8231
+ 'unknown_types': { <ftype_int>: [fname, ...] }
8232
+ }
8233
+ """
8234
+ if not isinstance(inarray, dict):
7662
8235
  return False
7663
- if(not listarrayfiles):
8236
+ if not inarray:
7664
8237
  return False
7665
- outarray = {'list': listarrayfiles, 'filetoid': {}, 'idtofile': {}, 'filetypes': {'directories': {'filetoid': {}, 'idtofile': {}}, 'files': {'filetoid': {}, 'idtofile': {}}, 'links': {'filetoid': {}, 'idtofile': {}}, 'symlinks': {'filetoid': {
7666
- }, 'idtofile': {}}, 'hardlinks': {'filetoid': {}, 'idtofile': {}}, 'character': {'filetoid': {}, 'idtofile': {}}, 'block': {'filetoid': {}, 'idtofile': {}}, 'fifo': {'filetoid': {}, 'idtofile': {}}, 'devices': {'filetoid': {}, 'idtofile': {}}}}
7667
- if(returnfp):
7668
- outarray.update({'fp': listarrayfiles['fp']})
7669
- lenlist = len(listarrayfiles['ffilelist'])
7670
- lcfi = 0
7671
- lcfx = int(listarrayfiles['fnumfiles'])
7672
- if(lenlist > listarrayfiles['fnumfiles'] or lenlist < listarrayfiles['fnumfiles']):
7673
- lcfx = int(lenlist)
7674
- else:
7675
- lcfx = int(listarrayfiles['fnumfiles'])
7676
- while(lcfi < lcfx):
7677
- filetoidarray = {listarrayfiles['ffilelist'][lcfi]
7678
- ['fname']: listarrayfiles['ffilelist'][lcfi]['fid']}
7679
- idtofilearray = {listarrayfiles['ffilelist'][lcfi]
7680
- ['fid']: listarrayfiles['ffilelist'][lcfi]['fname']}
7681
- outarray['filetoid'].update(filetoidarray)
7682
- outarray['idtofile'].update(idtofilearray)
7683
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 0 or listarrayfiles['ffilelist'][lcfi]['ftype'] == 7):
7684
- outarray['filetypes']['files']['filetoid'].update(filetoidarray)
7685
- outarray['filetypes']['files']['idtofile'].update(idtofilearray)
7686
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 1):
7687
- outarray['filetypes']['hardlinks']['filetoid'].update(
7688
- filetoidarray)
7689
- outarray['filetypes']['hardlinks']['idtofile'].update(
7690
- idtofilearray)
7691
- outarray['filetypes']['links']['filetoid'].update(filetoidarray)
7692
- outarray['filetypes']['links']['idtofile'].update(idtofilearray)
7693
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 2):
7694
- outarray['filetypes']['symlinks']['filetoid'].update(filetoidarray)
7695
- outarray['filetypes']['symlinks']['idtofile'].update(idtofilearray)
7696
- outarray['filetypes']['links']['filetoid'].update(filetoidarray)
7697
- outarray['filetypes']['links']['idtofile'].update(idtofilearray)
7698
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 3):
7699
- outarray['filetypes']['character']['filetoid'].update(
7700
- filetoidarray)
7701
- outarray['filetypes']['character']['idtofile'].update(
7702
- idtofilearray)
7703
- outarray['filetypes']['devices']['filetoid'].update(filetoidarray)
7704
- outarray['filetypes']['devices']['idtofile'].update(idtofilearray)
7705
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 4):
7706
- outarray['filetypes']['block']['filetoid'].update(filetoidarray)
7707
- outarray['filetypes']['block']['idtofile'].update(idtofilearray)
7708
- outarray['filetypes']['devices']['filetoid'].update(filetoidarray)
7709
- outarray['filetypes']['devices']['idtofile'].update(idtofilearray)
7710
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 5):
7711
- outarray['filetypes']['directories']['filetoid'].update(
7712
- filetoidarray)
7713
- outarray['filetypes']['directories']['idtofile'].update(
7714
- idtofilearray)
7715
- if(listarrayfiles['ffilelist'][lcfi]['ftype'] == 6):
7716
- outarray['filetypes']['symlinks']['filetoid'].update(filetoidarray)
7717
- outarray['filetypes']['symlinks']['idtofile'].update(idtofilearray)
7718
- outarray['filetypes']['devices']['filetoid'].update(filetoidarray)
7719
- outarray['filetypes']['devices']['idtofile'].update(idtofilearray)
7720
- lcfi = lcfi + 1
7721
- return outarray
8238
+
8239
+ # Buckets for categories
8240
+ def _bucket():
8241
+ return {"by_name": {}, "by_id": {}, "count": 0}
8242
+
8243
+ by_type = {}
8244
+ for cat in CATEGORY_ORDER:
8245
+ by_type[cat] = _bucket()
8246
+
8247
+ out = {
8248
+ "list": inarray,
8249
+ "fp": inarray.get("fp") if returnfp else None,
8250
+ "entries": {},
8251
+ "indexes": {
8252
+ "by_name": {},
8253
+ "by_type": by_type,
8254
+ },
8255
+ "counts": {"total": 0, "by_type": {}},
8256
+ "unknown_types": {},
8257
+ }
8258
+
8259
+ ffilelist = inarray.get("ffilelist") or []
8260
+ try:
8261
+ fnumfiles = int(inarray.get("fnumfiles", len(ffilelist)))
8262
+ except Exception:
8263
+ fnumfiles = len(ffilelist)
8264
+
8265
+ # Process only what's present
8266
+ total = min(len(ffilelist), fnumfiles)
8267
+
8268
+ def _add(cat, name, fid):
8269
+ b = by_type[cat]
8270
+ b["by_name"][name] = fid
8271
+ b["by_id"][fid] = name
8272
+ # Count is number of unique names in this category
8273
+ b["count"] = len(b["by_name"])
8274
+
8275
+ i = 0
8276
+ while i < total:
8277
+ e = ffilelist[i]
8278
+ name = e.get("fname")
8279
+ fid = e.get("fid")
8280
+ t = e.get("ftype")
8281
+
8282
+ if name is None or fid is None or t is None:
8283
+ i += 1
8284
+ continue
8285
+
8286
+ # Store canonical entry once, keyed by fid
8287
+ out["entries"][fid] = {"name": name, "type": t}
8288
+
8289
+ # Global reverse index for fast name -> id
8290
+ out["indexes"]["by_name"][name] = fid
8291
+
8292
+ # Base category
8293
+ base_cat = BASE_CATEGORY_BY_CODE.get(t)
8294
+ if base_cat is not None:
8295
+ _add(base_cat, name, fid)
8296
+ else:
8297
+ # Track unknown codes for visibility/forward-compat
8298
+ lst = out["unknown_types"].setdefault(t, [])
8299
+ if name not in lst:
8300
+ lst.append(name)
8301
+
8302
+ # Union categories
8303
+ for union_name, code_set in UNION_RULES:
8304
+ if t in code_set:
8305
+ _add(union_name, name, fid)
8306
+
8307
+ i += 1
8308
+
8309
+ # Counts
8310
+ out["counts"]["total"] = total
8311
+ for cat in CATEGORY_ORDER:
8312
+ out["counts"]["by_type"][cat] = by_type[cat]["count"]
8313
+
8314
+ return out
7722
8315
 
7723
8316
 
7724
8317
  def RePackCatFile(infile, outfile, fmttype="auto", compression="auto", compresswholefile=True, compressionlevel=None, compressionuselist=compressionlistalt, followlink=False, filestart=0, seekstart=0, seekend=0, checksumtype=["crc32", "crc32", "crc32", "crc32"], skipchecksum=False, extradata=[], jsondata={}, formatspecs=__file_format_dict__, seektoend=False, verbose=False, returnfp=False):
@@ -7727,7 +8320,7 @@ def RePackCatFile(infile, outfile, fmttype="auto", compression="auto", compressw
7727
8320
  else:
7728
8321
  if(infile != "-" and not isinstance(infile, bytes) and not hasattr(infile, "read") and not hasattr(infile, "write")):
7729
8322
  infile = RemoveWindowsPath(infile)
7730
- listarrayfiles = CatFileToArray(infile, "auto", filestart, seekstart, seekend, False, True, skipchecksum, formatspecs, seektoend, returnfp)
8323
+ listarrayfiles = CatFileToArray(infile, "auto", filestart, seekstart, seekend, False, True, True, skipchecksum, formatspecs, seektoend, returnfp)
7731
8324
  if(IsNestedDict(formatspecs) and fmttype in formatspecs):
7732
8325
  formatspecs = formatspecs[fmttype]
7733
8326
  elif(IsNestedDict(formatspecs) and fmttype not in formatspecs):
@@ -7840,11 +8433,11 @@ def RePackCatFile(infile, outfile, fmttype="auto", compression="auto", compressw
7840
8433
  fdev_major = format(
7841
8434
  int(listarrayfiles['ffilelist'][reallcfi]['fmajor']), 'x').lower()
7842
8435
  fseeknextfile = listarrayfiles['ffilelist'][reallcfi]['fseeknextfile']
7843
- if(len(listarrayfiles['ffilelist'][reallcfi]['fextralist']) > listarrayfiles['ffilelist'][reallcfi]['fextrafields'] and len(listarrayfiles['ffilelist'][reallcfi]['fextralist']) > 0):
8436
+ if(len(listarrayfiles['ffilelist'][reallcfi]['fextradata']) > listarrayfiles['ffilelist'][reallcfi]['fextrafields'] and len(listarrayfiles['ffilelist'][reallcfi]['fextradata']) > 0):
7844
8437
  listarrayfiles['ffilelist'][reallcfi]['fextrafields'] = len(
7845
- listarrayfiles['ffilelist'][reallcfi]['fextralist'])
8438
+ listarrayfiles['ffilelist'][reallcfi]['fextradata'])
7846
8439
  if(not followlink and len(extradata) <= 0):
7847
- extradata = listarrayfiles['ffilelist'][reallcfi]['fextralist']
8440
+ extradata = listarrayfiles['ffilelist'][reallcfi]['fextradata']
7848
8441
  if(not followlink and len(jsondata) <= 0):
7849
8442
  jsondata = listarrayfiles['ffilelist'][reallcfi]['fjsondata']
7850
8443
  fcontents = listarrayfiles['ffilelist'][reallcfi]['fcontents']
@@ -7923,10 +8516,10 @@ def RePackCatFile(infile, outfile, fmttype="auto", compression="auto", compressw
7923
8516
  fdev_minor = format(int(flinkinfo['fminor']), 'x').lower()
7924
8517
  fdev_major = format(int(flinkinfo['fmajor']), 'x').lower()
7925
8518
  fseeknextfile = flinkinfo['fseeknextfile']
7926
- if(len(flinkinfo['fextralist']) > flinkinfo['fextrafields'] and len(flinkinfo['fextralist']) > 0):
7927
- flinkinfo['fextrafields'] = len(flinkinfo['fextralist'])
8519
+ if(len(flinkinfo['fextradata']) > flinkinfo['fextrafields'] and len(flinkinfo['fextradata']) > 0):
8520
+ flinkinfo['fextrafields'] = len(flinkinfo['fextradata'])
7928
8521
  if(len(extradata) < 0):
7929
- extradata = flinkinfo['fextralist']
8522
+ extradata = flinkinfo['fextradata']
7930
8523
  if(len(jsondata) < 0):
7931
8524
  extradata = flinkinfo['fjsondata']
7932
8525
  fcontents = flinkinfo['fcontents']
@@ -7958,12 +8551,6 @@ def RePackCatFile(infile, outfile, fmttype="auto", compression="auto", compressw
7958
8551
  fcontents.close()
7959
8552
  lcfi = lcfi + 1
7960
8553
  reallcfi = reallcfi + 1
7961
- if(lcfx > 0):
7962
- try:
7963
- fp.write(AppendNullBytes(
7964
- ["0", "0"], formatspecs['format_delimiter']))
7965
- except OSError:
7966
- return False
7967
8554
  if(outfile == "-" or outfile is None or hasattr(outfile, "read") or hasattr(outfile, "write")):
7968
8555
  fp = CompressOpenFileAlt(
7969
8556
  fp, compression, compressionlevel, compressionuselist, formatspecs)
@@ -8027,7 +8614,7 @@ def UnPackCatFile(infile, outdir=None, followlink=False, filestart=0, seekstart=
8027
8614
  else:
8028
8615
  if(infile != "-" and not hasattr(infile, "read") and not hasattr(infile, "write") and not (sys.version_info[0] >= 3 and isinstance(infile, bytes))):
8029
8616
  infile = RemoveWindowsPath(infile)
8030
- listarrayfiles = CatFileToArray(infile, "auto", filestart, seekstart, seekend, False, True, skipchecksum, formatspecs, seektoend, returnfp)
8617
+ listarrayfiles = CatFileToArray(infile, "auto", filestart, seekstart, seekend, False, True, True, skipchecksum, formatspecs, seektoend, returnfp)
8031
8618
  if(not listarrayfiles):
8032
8619
  return False
8033
8620
  lenlist = len(listarrayfiles['ffilelist'])
@@ -8275,9 +8862,9 @@ def UnPackCatFile(infile, outdir=None, followlink=False, filestart=0, seekstart=
8275
8862
  return True
8276
8863
 
8277
8864
 
8278
- def UnPackCatFileString(instr, outdir=None, followlink=False, seekstart=0, seekend=0, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
8865
+ def UnPackCatFileString(instr, outdir=None, followlink=False, filestart=0, seekstart=0, seekend=0, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, returnfp=False):
8279
8866
  fp = MkTempFile(instr)
8280
- listarrayfiles = UnPackCatFile(fp, outdir, followlink, seekstart, seekend, skipchecksum, formatspecs, seektoend, verbose, returnfp)
8867
+ listarrayfiles = UnPackCatFile(fp, outdir, followlink, filestart, seekstart, seekend, skipchecksum, formatspecs, seektoend, verbose, returnfp)
8281
8868
  return listarrayfiles
8282
8869
 
8283
8870
  def ftype_to_str(ftype):
@@ -8348,10 +8935,60 @@ def CatFileListFiles(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0
8348
8935
  return True
8349
8936
 
8350
8937
 
8351
- def CatFileStringListFiles(instr, seekstart=0, seekend=0, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, newstyle=False, returnfp=False):
8938
+ def MultipleCatFileListFiles(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
8939
+ if(isinstance(infile, (list, tuple, ))):
8940
+ pass
8941
+ else:
8942
+ infile = [infile]
8943
+ outretval = {}
8944
+ for curfname in infile:
8945
+ outretval[curfname] = CatFileListFiles(infile, fmttype, filestart, seekstart, seekend, skipchecksum, formatspecs, seektoend, verbose, newstyle, returnfp)
8946
+ return outretval
8947
+
8948
+
8949
+ def StackedCatFileListFiles(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, newstyle=False, returnfp=False):
8950
+ outretval = []
8951
+ outstartfile = filestart
8952
+ outfsize = float('inf')
8953
+ while True:
8954
+ if outstartfile >= outfsize: # stop when function signals False
8955
+ break
8956
+ list_file_retu = ArchiveFileListFiles(infile, fmttype, outstartfile, seekstart, seekend, skipchecksum, formatspecs, seektoend, verbose, newstyle, True)
8957
+ if list_file_retu is False: # stop when function signals False
8958
+ outretval.append(list_file_retu)
8959
+ else:
8960
+ outretval.append(True)
8961
+ infile = list_file_retu
8962
+ outstartfile = infile.tell()
8963
+ try:
8964
+ infile.seek(0, 2)
8965
+ except OSError:
8966
+ SeekToEndOfFile(infile)
8967
+ except ValueError:
8968
+ SeekToEndOfFile(infile)
8969
+ outfsize = infile.tell()
8970
+ infile.seek(outstartfile, 0)
8971
+ if(returnfp):
8972
+ return infile
8973
+ else:
8974
+ infile.close()
8975
+ return outretval
8976
+
8977
+
8978
+ def MultipleStackedCatFileListFiles(infile, fmttype="auto", filestart=0, seekstart=0, seekend=0, listonly=False, contentasfile=True, uncompress=True, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, returnfp=False):
8979
+ if(isinstance(infile, (list, tuple, ))):
8980
+ pass
8981
+ else:
8982
+ infile = [infile]
8983
+ outretval = {}
8984
+ for curfname in infile:
8985
+ outretval[curfname] = StackedArchiveListFiles(curfname, fmttype, filestart, seekstart, seekend, listonly, contentasfile, uncompress, skipchecksum, formatspecs, seektoend, returnfp)
8986
+ return outretval
8987
+
8988
+
8989
+ def CatFileStringListFiles(instr, filestart=0, seekstart=0, seekend=0, skipchecksum=False, formatspecs=__file_format_multi_dict__, seektoend=False, verbose=False, newstyle=False, returnfp=False):
8352
8990
  fp = MkTempFile(instr)
8353
- listarrayfiles = CatFileListFiles(
8354
- instr, seekstart, seekend, skipchecksum, formatspecs, seektoend, verbose, newstyle, returnfp)
8991
+ listarrayfiles = CatFileListFiles(instr, "auto", filestart, seekstart, seekend, skipchecksum, formatspecs, seektoend, verbose, newstyle, returnfp)
8355
8992
  return listarrayfiles
8356
8993
 
8357
8994
 
@@ -8943,11 +9580,11 @@ def download_file_from_ftp_file(url):
8943
9580
  file_name = os.path.basename(unquote(urlparts.path))
8944
9581
  file_dir = os.path.dirname(unquote(urlparts.path))
8945
9582
  if(urlparts.username is not None):
8946
- ftp_username = urlparts.username
9583
+ ftp_username = unquote(urlparts.username)
8947
9584
  else:
8948
9585
  ftp_username = "anonymous"
8949
9586
  if(urlparts.password is not None):
8950
- ftp_password = urlparts.password
9587
+ ftp_password = unquote(urlparts.password)
8951
9588
  elif(urlparts.password is None and urlparts.username == "anonymous"):
8952
9589
  ftp_password = "anonymous"
8953
9590
  else:
@@ -8958,13 +9595,6 @@ def download_file_from_ftp_file(url):
8958
9595
  ftp = FTP_TLS()
8959
9596
  else:
8960
9597
  return False
8961
- if(urlparts.scheme == "sftp" or urlparts.scheme == "scp"):
8962
- if(__use_pysftp__):
8963
- return download_file_from_pysftp_file(url)
8964
- else:
8965
- return download_file_from_sftp_file(url)
8966
- elif(urlparts.scheme == "http" or urlparts.scheme == "https"):
8967
- return download_file_from_http_file(url)
8968
9598
  ftp_port = urlparts.port
8969
9599
  if(urlparts.port is None):
8970
9600
  ftp_port = 21
@@ -9041,11 +9671,11 @@ def upload_file_to_ftp_file(ftpfile, url):
9041
9671
  file_name = os.path.basename(unquote(urlparts.path))
9042
9672
  file_dir = os.path.dirname(unquote(urlparts.path))
9043
9673
  if(urlparts.username is not None):
9044
- ftp_username = urlparts.username
9674
+ ftp_username = unquote(urlparts.username)
9045
9675
  else:
9046
9676
  ftp_username = "anonymous"
9047
9677
  if(urlparts.password is not None):
9048
- ftp_password = urlparts.password
9678
+ ftp_password = unquote(urlparts.password)
9049
9679
  elif(urlparts.password is None and urlparts.username == "anonymous"):
9050
9680
  ftp_password = "anonymous"
9051
9681
  else:
@@ -9056,13 +9686,6 @@ def upload_file_to_ftp_file(ftpfile, url):
9056
9686
  ftp = FTP_TLS()
9057
9687
  else:
9058
9688
  return False
9059
- if(urlparts.scheme == "sftp" or urlparts.scheme == "scp"):
9060
- if(__use_pysftp__):
9061
- return upload_file_to_pysftp_file(url)
9062
- else:
9063
- return upload_file_to_sftp_file(url)
9064
- elif(urlparts.scheme == "http" or urlparts.scheme == "https"):
9065
- return False
9066
9689
  ftp_port = urlparts.port
9067
9690
  if(urlparts.port is None):
9068
9691
  ftp_port = 21
@@ -9160,8 +9783,8 @@ def download_file_from_http_file(url, headers=None, usehttp=__use_http_lib__):
9160
9783
  if headers is None:
9161
9784
  headers = {}
9162
9785
  urlparts = urlparse(url)
9163
- username = urlparts.username
9164
- password = urlparts.password
9786
+ username = unquote(urlparts.username)
9787
+ password = unquote(urlparts.password)
9165
9788
 
9166
9789
  # Rebuild URL without username and password
9167
9790
  netloc = urlparts.hostname or ''
@@ -9170,15 +9793,6 @@ def download_file_from_http_file(url, headers=None, usehttp=__use_http_lib__):
9170
9793
  rebuilt_url = urlunparse((urlparts.scheme, netloc, urlparts.path,
9171
9794
  urlparts.params, urlparts.query, urlparts.fragment))
9172
9795
 
9173
- # Handle SFTP/FTP
9174
- if urlparts.scheme == "sftp" or urlparts.scheme == "scp":
9175
- if __use_pysftp__:
9176
- return download_file_from_pysftp_file(url)
9177
- else:
9178
- return download_file_from_sftp_file(url)
9179
- elif urlparts.scheme == "ftp" or urlparts.scheme == "ftps":
9180
- return download_file_from_ftp_file(url)
9181
-
9182
9796
  # Create a temporary file object
9183
9797
  httpfile = MkTempFile()
9184
9798
 
@@ -9242,6 +9856,184 @@ def download_file_from_http_file(url, headers=None, usehttp=__use_http_lib__):
9242
9856
  return httpfile
9243
9857
 
9244
9858
 
9859
+ def upload_file_to_http_file(
9860
+ fileobj,
9861
+ url,
9862
+ method="POST", # "POST" or "PUT"
9863
+ headers=None,
9864
+ form=None, # dict of extra form fields → triggers multipart/form-data
9865
+ field_name="file", # form field name for the file content
9866
+ filename=None, # defaults to basename of URL path
9867
+ content_type="application/octet-stream",
9868
+ usehttp=__use_http_lib__, # 'requests' | 'httpx' | 'mechanize' | anything → urllib fallback
9869
+ ):
9870
+ """
9871
+ Py2+Py3 compatible HTTP/HTTPS upload.
9872
+
9873
+ - If `form` is provided (dict), uses multipart/form-data:
9874
+ * text fields from `form`
9875
+ * file part named by `field_name` with given `filename` and `content_type`
9876
+ - If `form` is None, uploads raw body as POST/PUT with Content-Type.
9877
+ - Returns True on HTTP 2xx, else False.
9878
+ """
9879
+ if headers is None:
9880
+ headers = {}
9881
+ method = (method or "POST").upper()
9882
+
9883
+ rebuilt_url, username, password = _rewrite_url_without_auth(url)
9884
+ filename = _guess_filename(url, filename)
9885
+
9886
+ # rewind if possible
9887
+ try:
9888
+ fileobj.seek(0)
9889
+ except Exception:
9890
+ pass
9891
+
9892
+ # ========== 1) requests (Py2+Py3) ==========
9893
+ if usehttp == 'requests' and haverequests:
9894
+ import requests
9895
+
9896
+ auth = (username, password) if (username or password) else None
9897
+
9898
+ if form is not None:
9899
+ # multipart/form-data
9900
+ files = {field_name: (filename, fileobj, content_type)}
9901
+ data = form or {}
9902
+ resp = requests.request(method, rebuilt_url, headers=headers, auth=auth,
9903
+ files=files, data=data, timeout=(5, 120))
9904
+ else:
9905
+ # raw body
9906
+ hdrs = {'Content-Type': content_type}
9907
+ hdrs.update(headers)
9908
+ # best-effort content-length (helps some servers)
9909
+ if hasattr(fileobj, 'seek') and hasattr(fileobj, 'tell'):
9910
+ try:
9911
+ cur = fileobj.tell()
9912
+ fileobj.seek(0, io.SEEK_END if hasattr(io, 'SEEK_END') else 2)
9913
+ size = fileobj.tell() - cur
9914
+ fileobj.seek(cur)
9915
+ hdrs.setdefault('Content-Length', str(size))
9916
+ except Exception:
9917
+ pass
9918
+ resp = requests.request(method, rebuilt_url, headers=hdrs, auth=auth,
9919
+ data=fileobj, timeout=(5, 300))
9920
+
9921
+ return (200 <= resp.status_code < 300)
9922
+
9923
+ # ========== 2) httpx (Py3 only) ==========
9924
+ if usehttp == 'httpx' and havehttpx and not PY2:
9925
+ import httpx
9926
+ auth = (username, password) if (username or password) else None
9927
+
9928
+ with httpx.Client(follow_redirects=True, timeout=60) as client:
9929
+ if form is not None:
9930
+ files = {field_name: (filename, fileobj, content_type)}
9931
+ data = form or {}
9932
+ resp = client.request(method, rebuilt_url, headers=headers, auth=auth,
9933
+ files=files, data=data)
9934
+ else:
9935
+ hdrs = {'Content-Type': content_type}
9936
+ hdrs.update(headers)
9937
+ resp = client.request(method, rebuilt_url, headers=hdrs, auth=auth,
9938
+ content=fileobj)
9939
+ return (200 <= resp.status_code < 300)
9940
+
9941
+ # ========== 3) mechanize (forms) → prefer requests if available ==========
9942
+ if usehttp == 'mechanize' and havemechanize:
9943
+ # mechanize is great for HTML forms, but file upload requires form discovery.
9944
+ # For a generic upload helper, prefer requests. If not available, fall through.
9945
+ try:
9946
+ import requests # noqa
9947
+ # delegate to requests path to ensure robust multipart handling
9948
+ return upload_file_to_http_file(
9949
+ fileobj, url, method=method, headers=headers,
9950
+ form=(form or {}), field_name=field_name,
9951
+ filename=filename, content_type=content_type,
9952
+ usehttp='requests'
9953
+ )
9954
+ except Exception:
9955
+ pass # fall through to urllib
9956
+
9957
+ # ========== 4) urllib fallback (Py2+Py3) ==========
9958
+ # multipart builder (no f-strings)
9959
+ boundary = ('----pyuploader-%s' % uuid.uuid4().hex)
9960
+
9961
+ if form is not None:
9962
+ # Build multipart body to a temp file-like (your MkTempFile())
9963
+ buf = MkTempFile()
9964
+
9965
+ def _w(s):
9966
+ buf.write(_to_bytes(s))
9967
+
9968
+ # text fields
9969
+ if form:
9970
+ for k, v in form.items():
9971
+ _w('--' + boundary + '\r\n')
9972
+ _w('Content-Disposition: form-data; name="%s"\r\n\r\n' % k)
9973
+ _w('' if v is None else (v if isinstance(v, (str, bytes)) else str(v)))
9974
+ _w('\r\n')
9975
+
9976
+ # file field
9977
+ _w('--' + boundary + '\r\n')
9978
+ _w('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (field_name, filename))
9979
+ _w('Content-Type: %s\r\n\r\n' % content_type)
9980
+
9981
+ try:
9982
+ fileobj.seek(0)
9983
+ except Exception:
9984
+ pass
9985
+ shutil.copyfileobj(fileobj, buf)
9986
+
9987
+ _w('\r\n')
9988
+ _w('--' + boundary + '--\r\n')
9989
+
9990
+ buf.seek(0)
9991
+ data = buf.read()
9992
+ hdrs = {'Content-Type': 'multipart/form-data; boundary=%s' % boundary}
9993
+ hdrs.update(headers)
9994
+ req = Request(rebuilt_url, data=data)
9995
+ # method override for Py3; Py2 Request ignores 'method' kw
9996
+ if not PY2:
9997
+ req.method = method # type: ignore[attr-defined]
9998
+ else:
9999
+ # raw body
10000
+ try:
10001
+ fileobj.seek(0)
10002
+ except Exception:
10003
+ pass
10004
+ data = fileobj.read()
10005
+ hdrs = {'Content-Type': content_type}
10006
+ hdrs.update(headers)
10007
+ req = Request(rebuilt_url, data=data)
10008
+ if not PY2:
10009
+ req.method = method # type: ignore[attr-defined]
10010
+
10011
+ for k, v in hdrs.items():
10012
+ req.add_header(k, v)
10013
+
10014
+ # Basic auth if present
10015
+ if username or password:
10016
+ pwd_mgr = HTTPPasswordMgrWithDefaultRealm()
10017
+ pwd_mgr.add_password(None, rebuilt_url, username, password)
10018
+ opener = build_opener(HTTPBasicAuthHandler(pwd_mgr))
10019
+ else:
10020
+ opener = build_opener()
10021
+
10022
+ # Py2 OpenerDirector.open takes timeout since 2.6; to be safe, avoid passing if it explodes
10023
+ try:
10024
+ resp = opener.open(req, timeout=60)
10025
+ except TypeError:
10026
+ resp = opener.open(req)
10027
+
10028
+ # Status code compat
10029
+ code = getattr(resp, 'status', None) or getattr(resp, 'code', None) or 0
10030
+ try:
10031
+ resp.close()
10032
+ except Exception:
10033
+ pass
10034
+ return (200 <= int(code) < 300)
10035
+
10036
+
9245
10037
  def download_file_from_http_string(url, headers=geturls_headers_pyfile_python_alt, usehttp=__use_http_lib__):
9246
10038
  httpfile = download_file_from_http_file(url, headers, usehttp)
9247
10039
  httpout = httpfile.read()
@@ -9260,19 +10052,15 @@ if(haveparamiko):
9260
10052
  else:
9261
10053
  sftp_port = urlparts.port
9262
10054
  if(urlparts.username is not None):
9263
- sftp_username = urlparts.username
10055
+ sftp_username = unquote(urlparts.username)
9264
10056
  else:
9265
10057
  sftp_username = "anonymous"
9266
10058
  if(urlparts.password is not None):
9267
- sftp_password = urlparts.password
10059
+ sftp_password = unquote(urlparts.password)
9268
10060
  elif(urlparts.password is None and urlparts.username == "anonymous"):
9269
10061
  sftp_password = "anonymous"
9270
10062
  else:
9271
10063
  sftp_password = ""
9272
- if(urlparts.scheme == "ftp"):
9273
- return download_file_from_ftp_file(url)
9274
- elif(urlparts.scheme == "http" or urlparts.scheme == "https"):
9275
- return download_file_from_http_file(url)
9276
10064
  if(urlparts.scheme != "sftp" and urlparts.scheme != "scp"):
9277
10065
  return False
9278
10066
  ssh = paramiko.SSHClient()
@@ -9280,7 +10068,7 @@ if(haveparamiko):
9280
10068
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
9281
10069
  try:
9282
10070
  ssh.connect(urlparts.hostname, port=sftp_port,
9283
- username=sftp_username, password=urlparts.password)
10071
+ username=sftp_username, password=sftp_password)
9284
10072
  except paramiko.ssh_exception.SSHException:
9285
10073
  return False
9286
10074
  except socket.gaierror:
@@ -9321,19 +10109,15 @@ if(haveparamiko):
9321
10109
  else:
9322
10110
  sftp_port = urlparts.port
9323
10111
  if(urlparts.username is not None):
9324
- sftp_username = urlparts.username
10112
+ sftp_username = unquote(urlparts.username)
9325
10113
  else:
9326
10114
  sftp_username = "anonymous"
9327
10115
  if(urlparts.password is not None):
9328
- sftp_password = urlparts.password
10116
+ sftp_password = unquote(urlparts.password)
9329
10117
  elif(urlparts.password is None and urlparts.username == "anonymous"):
9330
10118
  sftp_password = "anonymous"
9331
10119
  else:
9332
10120
  sftp_password = ""
9333
- if(urlparts.scheme == "ftp"):
9334
- return upload_file_to_ftp_file(sftpfile, url)
9335
- elif(urlparts.scheme == "http" or urlparts.scheme == "https"):
9336
- return False
9337
10121
  if(urlparts.scheme != "sftp" and urlparts.scheme != "scp"):
9338
10122
  return False
9339
10123
  ssh = paramiko.SSHClient()
@@ -9382,19 +10166,15 @@ if(havepysftp):
9382
10166
  else:
9383
10167
  sftp_port = urlparts.port
9384
10168
  if(urlparts.username is not None):
9385
- sftp_username = urlparts.username
10169
+ sftp_username = unquote(urlparts.username)
9386
10170
  else:
9387
10171
  sftp_username = "anonymous"
9388
10172
  if(urlparts.password is not None):
9389
- sftp_password = urlparts.password
10173
+ sftp_password = unquote(urlparts.password)
9390
10174
  elif(urlparts.password is None and urlparts.username == "anonymous"):
9391
10175
  sftp_password = "anonymous"
9392
10176
  else:
9393
10177
  sftp_password = ""
9394
- if(urlparts.scheme == "ftp"):
9395
- return download_file_from_ftp_file(url)
9396
- elif(urlparts.scheme == "http" or urlparts.scheme == "https"):
9397
- return download_file_from_http_file(url)
9398
10178
  if(urlparts.scheme != "sftp" and urlparts.scheme != "scp"):
9399
10179
  return False
9400
10180
  try:
@@ -9439,19 +10219,15 @@ if(havepysftp):
9439
10219
  else:
9440
10220
  sftp_port = urlparts.port
9441
10221
  if(urlparts.username is not None):
9442
- sftp_username = urlparts.username
10222
+ sftp_username = unquote(urlparts.username)
9443
10223
  else:
9444
10224
  sftp_username = "anonymous"
9445
10225
  if(urlparts.password is not None):
9446
- sftp_password = urlparts.password
10226
+ sftp_password = unquote(urlparts.password)
9447
10227
  elif(urlparts.password is None and urlparts.username == "anonymous"):
9448
10228
  sftp_password = "anonymous"
9449
10229
  else:
9450
10230
  sftp_password = ""
9451
- if(urlparts.scheme == "ftp"):
9452
- return upload_file_to_ftp_file(sftpfile, url)
9453
- elif(urlparts.scheme == "http" or urlparts.scheme == "https"):
9454
- return False
9455
10231
  if(urlparts.scheme != "sftp" and urlparts.scheme != "scp"):
9456
10232
  return False
9457
10233
  try:
@@ -9497,6 +10273,13 @@ def download_file_from_internet_file(url, headers=geturls_headers_pyfile_python_
9497
10273
  return download_file_from_pysftp_file(url)
9498
10274
  else:
9499
10275
  return download_file_from_sftp_file(url)
10276
+ elif(urlparts.scheme == "tcp" or urlparts.scheme == "udp"):
10277
+ outfile = MkTempFile()
10278
+ returnval = recv_via_url(outfile, url, recv_to_fileobj)
10279
+ if(not returnval):
10280
+ return False
10281
+ outfile.seek(0, 0)
10282
+ return outfile
9500
10283
  else:
9501
10284
  return False
9502
10285
  return False
@@ -9549,6 +10332,12 @@ def upload_file_to_internet_file(ifp, url):
9549
10332
  return upload_file_to_pysftp_file(ifp, url)
9550
10333
  else:
9551
10334
  return upload_file_to_sftp_file(ifp, url)
10335
+ elif(urlparts.scheme == "tcp" or urlparts.scheme == "udp"):
10336
+ ifp.seek(0, 0)
10337
+ returnval = send_via_url(ifp, url, send_from_fileobj)
10338
+ if(not returnval):
10339
+ return False
10340
+ return returnval
9552
10341
  else:
9553
10342
  return False
9554
10343
  return False
@@ -9557,7 +10346,7 @@ def upload_file_to_internet_file(ifp, url):
9557
10346
  def upload_file_to_internet_compress_file(ifp, url, compression="auto", compressionlevel=None, compressionuselist=compressionlistalt, formatspecs=__file_format_dict__):
9558
10347
  fp = CompressOpenFileAlt(
9559
10348
  fp, compression, compressionlevel, compressionuselist, formatspecs)
9560
- if(not catfileout):
10349
+ if(not archivefileout):
9561
10350
  return False
9562
10351
  fp.seek(0, 0)
9563
10352
  return upload_file_to_internet_file(fp, outfile)
@@ -9583,7 +10372,602 @@ def upload_file_to_internet_compress_string(ifp, url, compression="auto", compre
9583
10372
  internetfileo = MkTempFile(ifp)
9584
10373
  fp = CompressOpenFileAlt(
9585
10374
  internetfileo, compression, compressionlevel, compressionuselist, formatspecs)
9586
- if(not catfileout):
10375
+ if(not archivefileout):
9587
10376
  return False
9588
10377
  fp.seek(0, 0)
9589
10378
  return upload_file_to_internet_file(fp, outfile)
10379
+
10380
+
10381
+ # ---------- Core: send / recv ----------
10382
+ def send_from_fileobj(fileobj, host, port, proto="tcp", timeout=None,
10383
+ chunk_size=65536,
10384
+ use_ssl=False, ssl_verify=True, ssl_ca_file=None,
10385
+ ssl_certfile=None, ssl_keyfile=None, server_hostname=None,
10386
+ auth_user=None, auth_pass=None, auth_scope=u"",
10387
+ on_progress=None, rate_limit_bps=None, want_sha=True):
10388
+ """
10389
+ Send fileobj contents to (host, port) via TCP or UDP.
10390
+
10391
+ UDP behavior:
10392
+ - Computes total length and sha256 when possible.
10393
+ - Sends: AF1 (if auth) + 'LEN <n> [<sha>]\\n' + payload
10394
+ - If length unknown: stream payload, then 'HASH <sha>\\n' (if enabled), then 'DONE\\n'.
10395
+ - Uses small datagrams (<=1200B) to avoid fragmentation.
10396
+ """
10397
+ proto = (proto or "tcp").lower()
10398
+ total = 0
10399
+ port = int(port)
10400
+ if proto not in ("tcp", "udp"):
10401
+ raise ValueError("proto must be 'tcp' or 'udp'")
10402
+
10403
+ # ---------------- UDP ----------------
10404
+ if proto == "udp":
10405
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
10406
+ try:
10407
+ if timeout is not None:
10408
+ sock.settimeout(timeout)
10409
+
10410
+ # connect UDP for convenience
10411
+ try:
10412
+ sock.connect((host, port))
10413
+ connected = True
10414
+ except Exception:
10415
+ connected = False
10416
+
10417
+ # length + optional sha
10418
+ total_bytes, start_pos = _discover_len_and_reset(fileobj)
10419
+
10420
+ sha_hex = None
10421
+ if want_sha and total_bytes is not None:
10422
+ import hashlib
10423
+ h = hashlib.sha256()
10424
+ try:
10425
+ cur = fileobj.tell()
10426
+ except Exception:
10427
+ cur = None
10428
+ if start_pos is not None:
10429
+ try: fileobj.seek(start_pos, os.SEEK_SET)
10430
+ except Exception: pass
10431
+ _HSZ = 1024 * 1024
10432
+ while True:
10433
+ blk = fileobj.read(_HSZ)
10434
+ if not blk: break
10435
+ h.update(_to_bytes(blk))
10436
+ sha_hex = h.hexdigest()
10437
+ if start_pos is not None:
10438
+ try: fileobj.seek(start_pos, os.SEEK_SET)
10439
+ except Exception: pass
10440
+ elif cur is not None:
10441
+ try: fileobj.seek(cur, os.SEEK_SET)
10442
+ except Exception: pass
10443
+
10444
+ # optional AF1 (also carries len/sha, but we'll still send LEN for robustness)
10445
+ if auth_user is not None or auth_pass is not None:
10446
+ try:
10447
+ blob = build_auth_blob_v1(
10448
+ auth_user or u"", auth_pass or u"",
10449
+ scope=auth_scope, length=total_bytes, sha_hex=(sha_hex if want_sha else None)
10450
+ )
10451
+ except Exception:
10452
+ blob = _build_auth_blob_legacy(auth_user or b"", auth_pass or b"")
10453
+ if connected:
10454
+ sock.send(blob)
10455
+ # You may ignore the ack in UDP; keep try/except minimal
10456
+ try:
10457
+ resp = sock.recv(16)
10458
+ if resp != _OK:
10459
+ raise RuntimeError("UDP auth failed")
10460
+ except Exception:
10461
+ pass
10462
+ else:
10463
+ sock.sendto(blob, (host, port))
10464
+ try:
10465
+ resp, _ = sock.recvfrom(16)
10466
+ if resp != _OK:
10467
+ raise RuntimeError("UDP auth failed")
10468
+ except Exception:
10469
+ pass
10470
+
10471
+ # ALWAYS send LEN when length is known
10472
+ if total_bytes is not None:
10473
+ preface = b"LEN " + str(int(total_bytes)).encode("ascii")
10474
+ if want_sha and sha_hex:
10475
+ preface += b" " + sha_hex.encode("ascii")
10476
+ preface += b"\n"
10477
+ if connected: sock.send(preface)
10478
+ else: sock.sendto(preface, (host, port))
10479
+
10480
+ # payload stream
10481
+ UDP_PAYLOAD_MAX = 1200
10482
+ effective_chunk = min(int(chunk_size or 65536), UDP_PAYLOAD_MAX)
10483
+
10484
+ sent_so_far = 0
10485
+ last_cb_ts = monotonic()
10486
+ last_rate_ts = last_cb_ts
10487
+ last_rate_bytes = 0
10488
+
10489
+ rolling_h = None
10490
+ if want_sha and total_bytes is None:
10491
+ try:
10492
+ import hashlib
10493
+ rolling_h = hashlib.sha256()
10494
+ except Exception:
10495
+ rolling_h = None
10496
+
10497
+ while True:
10498
+ chunk = fileobj.read(effective_chunk)
10499
+ if not chunk:
10500
+ break
10501
+ b = _to_bytes(chunk)
10502
+ if rolling_h is not None:
10503
+ rolling_h.update(b)
10504
+ n = (sock.send(b) if connected else sock.sendto(b, (host, port)))
10505
+ total += n
10506
+ sent_so_far += n
10507
+
10508
+ if rate_limit_bps:
10509
+ sleep_s, last_rate_ts, last_rate_bytes = _progress_tick(
10510
+ sent_so_far, total_bytes, last_rate_ts, last_rate_bytes, rate_limit_bps
10511
+ )
10512
+ if sleep_s > 0.0:
10513
+ time.sleep(min(sleep_s, 0.25))
10514
+
10515
+ if on_progress and (monotonic() - last_cb_ts) >= 0.1:
10516
+ try: on_progress(sent_so_far, total_bytes)
10517
+ except Exception: pass
10518
+ last_cb_ts = monotonic()
10519
+
10520
+ # unknown-length trailers
10521
+ if total_bytes is None:
10522
+ if rolling_h is not None:
10523
+ try:
10524
+ th = rolling_h.hexdigest().encode("ascii")
10525
+ (sock.send(b"HASH " + th + b"\n") if connected
10526
+ else sock.sendto(b"HASH " + th + b"\n", (host, port)))
10527
+ except Exception:
10528
+ pass
10529
+ try:
10530
+ (sock.send(b"DONE\n") if connected else sock.sendto(b"DONE\n", (host, port)))
10531
+ except Exception:
10532
+ pass
10533
+
10534
+ finally:
10535
+ try: sock.close()
10536
+ except Exception: pass
10537
+ return total
10538
+
10539
+ # ---------------- TCP ----------------
10540
+ sock = _connect_stream(host, port, timeout)
10541
+ try:
10542
+ if use_ssl:
10543
+ if not _ssl_available():
10544
+ raise RuntimeError("SSL requested but 'ssl' module unavailable.")
10545
+ sock = _ssl_wrap_socket(sock, server_side=False,
10546
+ server_hostname=(server_hostname or host),
10547
+ verify=ssl_verify, ca_file=ssl_ca_file,
10548
+ certfile=ssl_certfile, keyfile=ssl_keyfile)
10549
+
10550
+ total_bytes, start_pos = _discover_len_and_reset(fileobj)
10551
+ sha_hex = None
10552
+ if want_sha and total_bytes is not None:
10553
+ try:
10554
+ import hashlib
10555
+ h = hashlib.sha256()
10556
+ cur = fileobj.tell()
10557
+ if start_pos is not None:
10558
+ fileobj.seek(start_pos, os.SEEK_SET)
10559
+ _HSZ = 1024 * 1024
10560
+ while True:
10561
+ blk = fileobj.read(_HSZ)
10562
+ if not blk: break
10563
+ h.update(_to_bytes(blk))
10564
+ sha_hex = h.hexdigest()
10565
+ fileobj.seek(cur, os.SEEK_SET)
10566
+ except Exception:
10567
+ sha_hex = None
10568
+
10569
+ if auth_user is not None or auth_pass is not None:
10570
+ try:
10571
+ blob = build_auth_blob_v1(
10572
+ auth_user or u"", auth_pass or u"",
10573
+ scope=auth_scope, length=total_bytes, sha_hex=(sha_hex if want_sha else None)
10574
+ )
10575
+ except Exception:
10576
+ blob = _build_auth_blob_legacy(auth_user or b"", auth_pass or b"")
10577
+ sock.sendall(blob)
10578
+ try:
10579
+ resp = sock.recv(16)
10580
+ if resp != _OK:
10581
+ raise RuntimeError("TCP auth failed")
10582
+ except Exception:
10583
+ pass
10584
+
10585
+ sent_so_far = 0
10586
+ last_cb_ts = monotonic()
10587
+ last_rate_ts = last_cb_ts
10588
+ last_rate_bytes = 0
10589
+
10590
+ use_sendfile = hasattr(sock, "sendfile") and hasattr(fileobj, "read")
10591
+ if use_sendfile:
10592
+ try:
10593
+ sent = sock.sendfile(fileobj)
10594
+ if isinstance(sent, int):
10595
+ total += sent
10596
+ sent_so_far += sent
10597
+ if on_progress:
10598
+ try: on_progress(sent_so_far, total_bytes)
10599
+ except Exception: pass
10600
+ else:
10601
+ raise RuntimeError("sendfile returned unexpected type")
10602
+ except Exception:
10603
+ while True:
10604
+ chunk = fileobj.read(chunk_size)
10605
+ if not chunk: break
10606
+ view = memoryview(_to_bytes(chunk))
10607
+ while view:
10608
+ n = sock.send(view); total += n; sent_so_far += n; view = view[n:]
10609
+ if rate_limit_bps:
10610
+ sleep_s, last_rate_ts, last_rate_bytes = _progress_tick(
10611
+ sent_so_far, total_bytes, last_rate_ts, last_rate_bytes, rate_limit_bps
10612
+ )
10613
+ if sleep_s > 0.0:
10614
+ time.sleep(min(sleep_s, 0.25))
10615
+ if on_progress and (monotonic() - last_cb_ts) >= 0.1:
10616
+ try: on_progress(sent_so_far, total_bytes)
10617
+ except Exception: pass
10618
+ last_cb_ts = monotonic()
10619
+ else:
10620
+ while True:
10621
+ chunk = fileobj.read(chunk_size)
10622
+ if not chunk: break
10623
+ view = memoryview(_to_bytes(chunk))
10624
+ while view:
10625
+ n = sock.send(view); total += n; sent_so_far += n; view = view[n:]
10626
+ if rate_limit_bps:
10627
+ sleep_s, last_rate_ts, last_rate_bytes = _progress_tick(
10628
+ sent_so_far, total_bytes, last_rate_ts, last_rate_bytes, rate_limit_bps
10629
+ )
10630
+ if sleep_s > 0.0:
10631
+ time.sleep(min(sleep_s, 0.25))
10632
+ if on_progress and (monotonic() - last_cb_ts) >= 0.1:
10633
+ try: on_progress(sent_so_far, total_bytes)
10634
+ except Exception: pass
10635
+ last_cb_ts = monotonic()
10636
+ finally:
10637
+ try: sock.shutdown(socket.SHUT_WR)
10638
+ except Exception: pass
10639
+ try: sock.close()
10640
+ except Exception: pass
10641
+ return total
10642
+
10643
+ def recv_to_fileobj(fileobj, host="", port=0, proto="tcp", timeout=None,
10644
+ max_bytes=None, chunk_size=65536, backlog=1,
10645
+ use_ssl=False, ssl_verify=True, ssl_ca_file=None,
10646
+ ssl_certfile=None, ssl_keyfile=None,
10647
+ require_auth=False, expected_user=None, expected_pass=None,
10648
+ total_timeout=None, expect_scope=None,
10649
+ on_progress=None, rate_limit_bps=None):
10650
+ """
10651
+ Receive bytes into fileobj over TCP/UDP.
10652
+
10653
+ UDP specifics:
10654
+ * Accepts 'LEN <n> [<sha>]\\n' and 'HASH <sha>\\n' control frames (unauth) or AF1 with len/sha.
10655
+ * If length unknown, accepts final 'DONE\\n' to end cleanly.
10656
+ """
10657
+ proto = (proto or "tcp").lower()
10658
+ port = int(port)
10659
+ total = 0
10660
+
10661
+ start_ts = time.time()
10662
+ def _time_left():
10663
+ if total_timeout is None:
10664
+ return None
10665
+ left = total_timeout - (time.time() - start_ts)
10666
+ return 0.0 if left <= 0 else left
10667
+ def _set_effective_timeout(socklike, base_timeout):
10668
+ left = _time_left()
10669
+ if left == 0.0:
10670
+ return False
10671
+ eff = base_timeout
10672
+ if left is not None:
10673
+ eff = left if eff is None else min(eff, left)
10674
+ if eff is not None:
10675
+ try:
10676
+ socklike.settimeout(eff)
10677
+ except Exception:
10678
+ pass
10679
+ return True
10680
+
10681
+ if proto not in ("tcp", "udp"):
10682
+ raise ValueError("proto must be 'tcp' or 'udp'")
10683
+
10684
+ # ---------------- UDP server ----------------
10685
+ if proto == "udp":
10686
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
10687
+ authed_addr = None
10688
+ expected_len = None
10689
+ expected_sha = None
10690
+
10691
+ try:
10692
+ sock.bind(("", port))
10693
+ if timeout is None:
10694
+ try: sock.settimeout(10.0)
10695
+ except Exception: pass
10696
+
10697
+ recvd_so_far = 0
10698
+ last_cb_ts = monotonic()
10699
+ last_rate_ts = last_cb_ts
10700
+ last_rate_bytes = 0
10701
+
10702
+ while True:
10703
+ if _time_left() == 0.0:
10704
+ if expected_len is not None and total < expected_len:
10705
+ raise RuntimeError("UDP receive aborted by total_timeout before full payload received")
10706
+ break
10707
+ if (max_bytes is not None) and (total >= max_bytes):
10708
+ break
10709
+
10710
+ if not _set_effective_timeout(sock, timeout):
10711
+ if expected_len is not None and total < expected_len:
10712
+ raise RuntimeError("UDP receive timed out before full payload received")
10713
+ if expected_len is None and total > 0:
10714
+ raise RuntimeError("UDP receive timed out with unknown length; partial data")
10715
+ if expected_len is None and total == 0:
10716
+ raise RuntimeError("UDP receive: no packets received before timeout (is the sender running?)")
10717
+ break
10718
+
10719
+ try:
10720
+ data, addr = sock.recvfrom(chunk_size)
10721
+ except socket.timeout:
10722
+ if expected_len is not None and total < expected_len:
10723
+ raise RuntimeError("UDP receive idle-timeout before full payload received")
10724
+ if expected_len is None and total > 0:
10725
+ raise RuntimeError("UDP receive idle-timeout with unknown length; partial data")
10726
+ if expected_len is None and total == 0:
10727
+ raise RuntimeError("UDP receive: no packets received before timeout (is the sender running?)")
10728
+ break
10729
+
10730
+ if not data:
10731
+ continue
10732
+
10733
+ # (0) Control frames FIRST: LEN / HASH / DONE
10734
+ if data.startswith(b"LEN ") and expected_len is None:
10735
+ try:
10736
+ parts = data.strip().split()
10737
+ n = int(parts[1])
10738
+ expected_len = (None if n < 0 else n)
10739
+ if len(parts) >= 3:
10740
+ expected_sha = parts[2].decode("ascii")
10741
+ except Exception:
10742
+ expected_len = None
10743
+ expected_sha = None
10744
+ continue
10745
+
10746
+ if data.startswith(b"HASH "):
10747
+ try:
10748
+ expected_sha = data.strip().split()[1].decode("ascii")
10749
+ except Exception:
10750
+ expected_sha = None
10751
+ continue
10752
+
10753
+ if data == b"DONE\n":
10754
+ break
10755
+
10756
+ # (1) Auth (AF1 preferred; legacy fallback)
10757
+ if authed_addr is None and require_auth:
10758
+ ok = False
10759
+ v_ok, v_user, v_scope, _r, v_len, v_sha = verify_auth_blob_v1(
10760
+ data, expected_user=expected_user, secret=expected_pass,
10761
+ max_skew=600, expect_scope=expect_scope
10762
+ )
10763
+ if v_ok:
10764
+ ok = True
10765
+ if expected_len is None:
10766
+ expected_len = v_len
10767
+ if expected_sha is None:
10768
+ expected_sha = v_sha
10769
+ else:
10770
+ user, pw = _parse_auth_blob_legacy(data)
10771
+ ok = (user is not None and
10772
+ (expected_user is None or user == _to_bytes(expected_user)) and
10773
+ (expected_pass is None or pw == _to_bytes(expected_pass)))
10774
+ try:
10775
+ sock.sendto((_OK if ok else _NO), addr)
10776
+ except Exception:
10777
+ pass
10778
+ if ok:
10779
+ authed_addr = addr
10780
+ continue
10781
+
10782
+ if require_auth and addr != authed_addr:
10783
+ continue
10784
+
10785
+ # (2) Payload
10786
+ fileobj.write(data)
10787
+ try: fileobj.flush()
10788
+ except Exception: pass
10789
+ total += len(data)
10790
+ recvd_so_far += len(data)
10791
+
10792
+ if rate_limit_bps:
10793
+ sleep_s, last_rate_ts, last_rate_bytes = _progress_tick(
10794
+ recvd_so_far, expected_len, last_rate_ts, last_rate_bytes, rate_limit_bps
10795
+ )
10796
+ if sleep_s > 0.0:
10797
+ time.sleep(min(sleep_s, 0.25))
10798
+
10799
+ if on_progress and (monotonic() - last_cb_ts) >= 0.1:
10800
+ try: on_progress(recvd_so_far, expected_len)
10801
+ except Exception: pass
10802
+ last_cb_ts = monotonic()
10803
+
10804
+ if expected_len is not None and total >= expected_len:
10805
+ break
10806
+
10807
+ # Post-conditions
10808
+ if expected_len is not None and total != expected_len:
10809
+ raise RuntimeError("UDP receive incomplete: got %d of %s bytes" % (total, expected_len))
10810
+
10811
+ if expected_sha:
10812
+ import hashlib
10813
+ try:
10814
+ cur = fileobj.tell(); fileobj.seek(0)
10815
+ except Exception:
10816
+ cur = None
10817
+ h = hashlib.sha256(); _HSZ = 1024 * 1024
10818
+ while True:
10819
+ blk = fileobj.read(_HSZ)
10820
+ if not blk: break
10821
+ h.update(_to_bytes(blk))
10822
+ got = h.hexdigest()
10823
+ if cur is not None:
10824
+ try: fileobj.seek(cur)
10825
+ except Exception: pass
10826
+ if got != expected_sha:
10827
+ raise RuntimeError("UDP checksum mismatch: got %s expected %s" % (got, expected_sha))
10828
+
10829
+ finally:
10830
+ try: sock.close()
10831
+ except Exception: pass
10832
+ return total
10833
+
10834
+ # ---------------- TCP server ----------------
10835
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10836
+ try:
10837
+ try: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
10838
+ except Exception: pass
10839
+ srv.bind((host or "", port))
10840
+ srv.listen(int(backlog) if backlog else 1)
10841
+
10842
+ if not _set_effective_timeout(srv, timeout):
10843
+ return 0
10844
+ try:
10845
+ conn, _peer = srv.accept()
10846
+ except socket.timeout:
10847
+ return 0
10848
+
10849
+ if use_ssl:
10850
+ if not _ssl_available():
10851
+ try: conn.close()
10852
+ except Exception: pass
10853
+ raise RuntimeError("SSL requested but 'ssl' module unavailable.")
10854
+ if not ssl_certfile:
10855
+ try: conn.close()
10856
+ except Exception: pass
10857
+ raise ValueError("TLS server requires ssl_certfile (and usually ssl_keyfile).")
10858
+ conn = _ssl_wrap_socket(conn, server_side=True, server_hostname=None,
10859
+ verify=ssl_verify, ca_file=ssl_ca_file,
10860
+ certfile=ssl_certfile, keyfile=ssl_keyfile)
10861
+
10862
+ recvd_so_far = 0
10863
+ last_cb_ts = monotonic()
10864
+ last_rate_ts = last_cb_ts
10865
+ last_rate_bytes = 0
10866
+
10867
+ try:
10868
+ if require_auth:
10869
+ if not _set_effective_timeout(conn, timeout):
10870
+ return 0
10871
+ try:
10872
+ preface = conn.recv(2048)
10873
+ except socket.timeout:
10874
+ try: conn.sendall(_NO)
10875
+ except Exception: pass
10876
+ return 0
10877
+
10878
+ ok = False
10879
+ v_ok, v_user, v_scope, _r, v_len, v_sha = verify_auth_blob_v1(
10880
+ preface or b"", expected_user=expected_user, secret=expected_pass,
10881
+ max_skew=600, expect_scope=expect_scope
10882
+ )
10883
+ if v_ok:
10884
+ ok = True
10885
+ else:
10886
+ user, pw = _parse_auth_blob_legacy(preface or b"")
10887
+ ok = (user is not None and
10888
+ (expected_user is None or user == _to_bytes(expected_user)) and
10889
+ (expected_pass is None or pw == _to_bytes(expected_pass)))
10890
+
10891
+ try: conn.sendall(_OK if ok else _NO)
10892
+ except Exception: pass
10893
+ if not ok:
10894
+ return 0
10895
+
10896
+ while True:
10897
+ if _time_left() == 0.0: break
10898
+ if (max_bytes is not None) and (total >= max_bytes): break
10899
+
10900
+ if not _set_effective_timeout(conn, timeout):
10901
+ break
10902
+ try:
10903
+ data = conn.recv(chunk_size)
10904
+ except socket.timeout:
10905
+ break
10906
+ if not data:
10907
+ break
10908
+
10909
+ fileobj.write(data)
10910
+ try: fileobj.flush()
10911
+ except Exception: pass
10912
+ total += len(data)
10913
+ recvd_so_far += len(data)
10914
+
10915
+ if rate_limit_bps:
10916
+ sleep_s, last_rate_ts, last_rate_bytes = _progress_tick(
10917
+ recvd_so_far, max_bytes, last_rate_ts, last_rate_bytes, rate_limit_bps
10918
+ )
10919
+ if sleep_s > 0.0:
10920
+ time.sleep(min(sleep_s, 0.25))
10921
+
10922
+ if on_progress and (monotonic() - last_cb_ts) >= 0.1:
10923
+ try: on_progress(recvd_so_far, max_bytes)
10924
+ except Exception: pass
10925
+ last_cb_ts = monotonic()
10926
+ finally:
10927
+ try: conn.shutdown(socket.SHUT_RD)
10928
+ except Exception: pass
10929
+ try: conn.close()
10930
+ except Exception: pass
10931
+ finally:
10932
+ try: srv.close()
10933
+ except Exception: pass
10934
+
10935
+ return total
10936
+
10937
+ # ---------- URL drivers ----------
10938
+ def send_via_url(fileobj, url, send_from_fileobj_func=send_from_fileobj):
10939
+ """
10940
+ Use URL options to drive the sender. Returns bytes sent.
10941
+ """
10942
+ parts, o = _parse_net_url(url)
10943
+ use_auth = (o["user"] is not None and o["pw"] is not None) or o["force_auth"]
10944
+ return send_from_fileobj_func(
10945
+ fileobj,
10946
+ o["host"], o["port"], proto=o["proto"],
10947
+ timeout=o["timeout"], chunk_size=o["chunk_size"],
10948
+ use_ssl=o["use_ssl"], ssl_verify=o["ssl_verify"],
10949
+ ssl_ca_file=o["ssl_ca_file"], ssl_certfile=o["ssl_certfile"], ssl_keyfile=o["ssl_keyfile"],
10950
+ server_hostname=o["server_hostname"],
10951
+ auth_user=(o["user"] if use_auth else None),
10952
+ auth_pass=(o["pw"] if use_auth else None),
10953
+ auth_scope=o.get("path", u""),
10954
+ want_sha=o["want_sha"], # <— pass through
10955
+ )
10956
+
10957
+ def recv_via_url(fileobj, url, recv_to_fileobj_func=recv_to_fileobj):
10958
+ """
10959
+ Use URL options to drive the receiver. Returns bytes received.
10960
+ """
10961
+ parts, o = _parse_net_url(url)
10962
+ require_auth = (o["user"] is not None and o["pw"] is not None) or o["force_auth"]
10963
+ return recv_to_fileobj_func(
10964
+ fileobj,
10965
+ o["host"], o["port"], proto=o["proto"],
10966
+ timeout=o["timeout"], total_timeout=o["total_timeout"],
10967
+ chunk_size=o["chunk_size"],
10968
+ use_ssl=o["use_ssl"], ssl_verify=o["ssl_verify"],
10969
+ ssl_ca_file=o["ssl_ca_file"], ssl_certfile=o["ssl_certfile"], ssl_keyfile=o["ssl_keyfile"],
10970
+ require_auth=require_auth,
10971
+ expected_user=o["user"], expected_pass=o["pw"],
10972
+ expect_scope=o.get("path", u""),
10973
+ )