Files
copyparty/scripts/sfx.py
T
2026-09-06 18:22:52 +00:00

515 lines
12 KiB
Python

#!/usr/bin/env python3
# coding: L1
from __future__ import division, print_function, unicode_literals
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
import subprocess as sp
# skip 1
#
# py2sfx (sfx.py) - bundle python-modules into an executable sfx.py
# (c)2020, ed <oss@ocv.me>, MIT-licensed, originally from copyparty:
# https://github.com/9001/copyparty/blob/hovudstraum/scripts/sfx.py
#
# ( no need to include the remaining lines of this comment-block
# for attribution; the rest is just for context )
#
# to create an sfx, use this:
# https://github.com/9001/copyparty/blob/hovudstraum/scripts/make-sfx.sh
#
# wanna steal this for your own project? then maybe
# see the r0c version, since that's slightly simpler:
# https://github.com/9001/r0c/blob/master/scripts/sfx.py
#
# skip 0
"""
run me with python 2.7 or 3.3+ to unpack and run copyparty
this is an SFX (self-extracting) tar.gz archive;
see https://copyparty.eu/sfx-wtf/
"""
# fmt: # set by make-sfx.sh
VER = None
SIZE = None
CKSUM = None
STAMP = None
sys.dont_write_bytecode = True
OP = os.path
ENV = os.environ
v = sys.version_info
PY2 = v < (3,)
PY37 = v > (3, 7)
WINDOWS = sys.platform in ["win32", "msys"]
me = OP.abspath(OP.realpath(__file__))
def eprint(*a, **ka):
ka["file"] = sys.stderr
print(*a, **ka)
def msg(*a, **ka):
if a:
a = ["[SFX]", a[0]] + list(a[1:])
eprint(*a, **ka)
# skip 1
def testptn1():
"""test: creates a test-pattern for encode()"""
import struct
buf = b""
for c in range(256):
buf += struct.pack("B", c)
yield buf
def testptn2():
import struct
for a in range(256):
if a % 16 == 0:
msg(a)
for b in range(256):
buf = b""
for c in range(256):
buf += struct.pack("BBBB", a, b, c, b)
yield buf
def testptn3():
with open("C:/Users/ed/Downloads/python-3.8.1-amd64.exe", "rb", 512 * 1024) as f:
while True:
buf = f.read(512 * 1024)
if not buf:
break
yield buf
testptn = testptn2
def testchk(cdata):
"""test: verifies that `data` yields testptn"""
import struct
cbuf = b""
mbuf = b""
checked = 0
t0 = time.time()
mdata = testptn()
while True:
if not mbuf:
try:
mbuf += next(mdata)
except:
break
if not cbuf:
try:
cbuf += next(cdata)
except:
expect = mbuf[:8]
expect = "".join(
" {:02x}".format(x)
for x in struct.unpack("B" * len(expect), expect)
)
raise Exception(
"truncated at {}, expected{}".format(checked + len(cbuf), expect)
)
ncmp = min(len(cbuf), len(mbuf))
# msg("checking {:x}H bytes, {:x}H ok so far".format(ncmp, checked))
for n in range(ncmp):
checked += 1
if cbuf[n] != mbuf[n]:
expect = mbuf[n : n + 8]
expect = "".join(
" {:02x}".format(x)
for x in struct.unpack("B" * len(expect), expect)
)
cc = struct.unpack(b"B", cbuf[n : n + 1])[0]
raise Exception(
"byte {:x}H bad, got {:02x}, expected{}".format(checked, cc, expect)
)
cbuf = cbuf[ncmp:]
mbuf = mbuf[ncmp:]
td = time.time() - t0
txt = "all {}d bytes OK in {:.3f} sec, {:.3f} MB/s".format(
checked, td, (checked / (1024 * 1024.0)) / td
)
msg(txt)
def encode(data, size, cksum, ver, ts):
"""creates a new sfx; `data` should yield bufs to attach"""
nb = 0
nin = 0
nout = 0
skip = False
with open(me, "rb") as fi:
unpk = ""
src = fi.read().replace(b"\r", b"").rstrip(b"\n").decode("utf-8")
for ln in src.split("\n"):
if ln.endswith("# skip 0"):
skip = False
nb = 9
continue
if ln.endswith("# skip 1") or skip:
skip = True
continue
if ln.strip().startswith("# fmt: "):
continue
if ln:
nb = 0
else:
nb += 1
if nb > 2:
continue
unpk += ln + "\n"
for k, v in [
["VER", '"' + ver + '"'],
["SIZE", size],
["CKSUM", '"' + cksum + '"'],
["STAMP", ts],
]:
v1 = "\n{} = None\n".format(k)
v2 = "\n{} = {}\n".format(k, v)
unpk = unpk.replace(v1, v2)
unpk = unpk.replace("\n ", "\n\t")
for _ in range(16):
unpk = unpk.replace("\t ", "\t\t")
with open("sfx.out", "wb") as f:
f.write(unpk.encode("utf-8").rstrip(b"\n") + b"\n\n\n# eof")
for buf in data:
ebuf = (
buf.replace(b"?", b"??")
.replace(b"\x00", b"?0")
.replace(b"\r", b"?r")
.replace(b"\n", b"?n")
)
nin += len(buf)
nout += len(ebuf)
while ebuf:
ep = 4090
while True:
a = ebuf.rfind(b"?", 0, ep)
if a < 0 or ep - a > 2:
break
ep = a
buf = ebuf[:ep]
ebuf = ebuf[ep:]
f.write(b"\n#" + buf)
f.write(b"\n\n")
msg("wrote {:x}H bytes ({:x}H after encode)".format(nin, nout))
def makesfx(tar_src, ver, ts):
sz = OP.getsize(tar_src)
cksum = hashfile(tar_src)
encode(yieldfile(tar_src), sz, cksum, ver, ts)
# skip 0
def u8(gen):
try:
for s in gen:
yield s.decode("utf-8", "ignore")
except:
yield s
for s in gen:
yield s
def yieldfile(fn):
s = 0x10000
with open(fn, "rb", s * 4) as f:
for block in iter(lambda: f.read(s), b""):
yield block
def hashfile(fn):
h = hashlib.sha1()
for block in yieldfile(fn):
h.update(block)
return h.hexdigest()[:24]
def unpack(top):
"""extracts the archive"""
name = "pe-copyparty"
try:
name += ".%s" % os.geteuid()
except:
pass
tag = "v%s" % STAMP
opj = OP.join
ofe = OP.exists
if not ofe(top):
os.mkdir(top)
final = opj(top, name)
san = opj(final, "copyparty/up2k.py")
for suf in range(0, 9001):
withpid = "%s.%d.%s" % (name, os.getpid(), suf)
mine = opj(top, withpid)
if not ofe(mine):
break
tar = opj(mine, "tar")
try:
if tag in os.listdir(final) and ofe(san):
msg("found early")
return final
except:
pass
sz = 0
os.mkdir(mine)
with open(tar, "wb") as f:
for buf in get_payload():
sz += len(buf)
f.write(buf)
ck = hashfile(tar)
if ck != CKSUM:
t = "\n\nexpected %s (%d byte)\nobtained %s (%d byte)\nsfx corrupt"
raise Exception(t % (CKSUM, SIZE, ck, sz))
with tarfile.open(tar, "r:bz2") as tf:
# this is traversal-safe
# skip 1
# since it will never process user-provided data;
# the only possible input is a single tar.bz2
# which gets hardcoded into this script at build stage
# skip 0
try:
tf.extractall(mine, filter="tar")
except TypeError:
tf.extractall(mine)
os.remove(tar)
with open(opj(mine, tag), "wb") as f:
f.write(b"h\n")
try:
if tag in os.listdir(final) and ofe(san):
msg("found late")
return final
except:
pass
try:
if OP.islink(final):
os.remove(final)
else:
shutil.rmtree(final)
except:
pass
try:
os.symlink(mine, final)
except:
try:
os.rename(mine, final)
return final
except:
msg("reloc fail,", mine)
return mine
def get_payload():
"""decodes the binary data"""
with open(me, "rb") as f:
buf = f.read().rstrip(b"\r\n")
ptn = b"\n# eof\n#"
a = buf.find(ptn)
if a < 0:
raise Exception("corrupt sfx: no eof marker")
esc = {b"??": b"?", b"?r": b"\r", b"?n": b"\n", b"?0": b"\x00"}
buf = buf[a + len(ptn) :].replace(b"\n#", b"")
p = 0
while buf:
a = buf.find(b"?", p)
if a < 0:
yield buf[p:]
break
elif a == p:
yield esc[buf[p : p + 2]]
p += 2
else:
yield buf[p:a]
p = a
def confirm(rv):
msg()
msg("retcode", rv if rv else traceback.format_exc())
if WINDOWS:
msg("*** hit enter to exit ***")
try:
raw_input() if PY2 else input()
except:
pass
sys.exit(rv or 1)
def run(tmp, j2, ftp):
msg("jinja2:", j2 or "bundled")
msg("pyftpd:", ftp or "bundled")
msg("sfxdir:", tmp)
msg()
sys.argv.append("--sfx-tpoke=" + tmp)
ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"), (PY37, "py37"))
ld = [OP.join(tmp, b) for a, b in ld if not a]
# skip 1
# enable this to dynamically remove type hints at startup,
# in case a future python version can use them for performance
if sys.version_info < (3, 10) and False:
sys.path.insert(0, ld[0])
from strip_hints.a import uh
uh(tmp + "/copyparty")
# skip 0
if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]):
run_s(ld)
else:
run_i(ld)
def run_i(ld):
for x in ld:
sys.path.insert(0, x)
ENV["PRTY_NO_IMPRESO"] = "1"
from copyparty.__main__ import main as p
p()
def run_s(ld):
# fmt: off
c = "import sys,runpy;" + "".join(['sys.path.insert(0,r"' + x.replace("\\", "/") + '");' for x in ld]) + 'runpy.run_module("copyparty",run_name="__main__")'
c = [str(x) for x in [sys.executable, "-c", c] + list(sys.argv[1:])]
# fmt: on
msg("\n", c, "\n")
p = sp.Popen(c)
def bye(*a):
p.send_signal(signal.SIGINT)
signal.signal(signal.SIGTERM, bye)
p.wait()
raise SystemExit(p.returncode)
def main():
if "--versionb" in sys.argv:
return print(VER)
sysver = str(sys.version).replace("\n", "\n" + " " * 18)
pktime = time.strftime("%Y-%m-%d, %H:%M:%S", time.gmtime(STAMP))
msg()
msg(" this is: copyparty", VER)
msg(" packed at:", pktime, "UTC,", STAMP)
msg("archive is:", me)
msg("python bin:", sys.executable)
msg("python ver:", platform.python_implementation(), sysver)
msg()
arg = ""
try:
arg = sys.argv[1]
except:
pass
# skip 1
if arg == "--sfx-testgen":
return encode(testptn(), 1, "x", "x", 1)
if arg == "--sfx-testchk":
return testchk(get_payload())
if arg == "--sfx-make":
tar, ver, ts = sys.argv[2:]
return makesfx(tar, ver, ts)
# skip 0
try:
if WINDOWS:
x
z = unpack(ENV.get("PRTY_XD") or OP.expanduser("~/.cache"))
except:
z = unpack(tempfile.gettempdir())
tmp = OP.realpath(z)
try:
from jinja2 import __version__ as j2
except:
j2 = None
try:
from pyftpdlib.__init__ import __ver__ as ftp
except:
ftp = None
try:
run(tmp, j2, ftp)
except SystemExit as ex:
c = ex.code
if c not in [0, -15]:
confirm(ex.code)
except KeyboardInterrupt:
pass
except:
confirm(0)
if __name__ == "__main__":
main()
# skip 1
# python sfx.py --sfx-testgen && python test.py --sfx-testchk
# c:\Python27\python.exe sfx.py --sfx-testgen && c:\Python27\python.exe test.py --sfx-testchk