Filza 27.2 worse_query: public and developer builds

This commit is contained in:
admin committed 2026-11-11 03:40:37 +08:00
commit 91a6adf723
267 files changed
+42015

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
#!/bin/zsh
# Build the developer Filza 27.2 IPA: public features plus the full terminal,
# apt/dpkg, SSH server, mount, archive extraction and respring.
#
# Required environment:
# BASE_IPA path to FilzaSlop-v1.2.0-unsigned.ipa
# ASHELL_IPA path to a signed/unsigned a-Shell IPA that supplies the
# command frameworks and resources
# WQ_APP_GROUPS at least one App Group from the signing profile
# Optional:
# PROFILE, ENTITLEMENTS, SIGN_IDENTITY, KEYCHAIN
# BUNDLE_ID (default com.jbjf.filzanb)
# APP_DISPLAY_NAME (default filza27.2)
set -euo pipefail
ROOT="${0:A:h:h}"
BASE_IPA="${BASE_IPA:-$ROOT/base/FilzaSlop-v1.2.0-unsigned.ipa}"
OUTPUT="${1:-$ROOT/build/filza_develop.ipa}"
export BUNDLE_ID="${BUNDLE_ID:-com.jbjf.filzanb}"
export APP_DISPLAY_NAME="${APP_DISPLAY_NAME:-filza27.2}"
export FILZASLOP_VERSION="${FILZASLOP_VERSION:-wq-27.2-developer}"
export WQ_TERMINAL=full
export ASHELL_IPA="${ASHELL_IPA:-$HOME/Downloads/Terminal.ipa}"
: "${WQ_APP_GROUPS:?set WQ_APP_GROUPS to the App Group(s) in your profile}"
[[ -f "$ASHELL_IPA" ]] || {
echo "a-Shell IPA not found: $ASHELL_IPA" >&2
echo "Set ASHELL_IPA to a-Shell (or a-Shell mini) IPA." >&2
exit 66
}
mkdir -p "${OUTPUT:h}"
exec "$ROOT/scripts/build_wq_ipa.sh" "$BASE_IPA" "$OUTPUT"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/zsh
# Build the public Filza 27.2 IPA (no terminal, SSH server or package CLI).
#
# Required environment:
# BASE_IPA path to FilzaSlop-v1.2.0-unsigned.ipa
# WQ_APP_GROUPS at least one App Group from the signing profile
# Optional:
# PROFILE, ENTITLEMENTS, SIGN_IDENTITY, KEYCHAIN
# BUNDLE_ID (default com.jbjf.filza)
# APP_DISPLAY_NAME (default filza27.2)
set -euo pipefail
ROOT="${0:A:h:h}"
BASE_IPA="${BASE_IPA:-$ROOT/base/FilzaSlop-v1.2.0-unsigned.ipa}"
OUTPUT="${1:-$ROOT/build/filza_public.ipa}"
export BUNDLE_ID="${BUNDLE_ID:-com.jbjf.filza}"
export APP_DISPLAY_NAME="${APP_DISPLAY_NAME:-filza27.2}"
export FILZASLOP_VERSION="${FILZASLOP_VERSION:-wq-27.2-public}"
: "${WQ_APP_GROUPS:?set WQ_APP_GROUPS to the App Group(s) in your profile}"
unset WQ_TERMINAL
mkdir -p "${OUTPUT:h}"
exec "$ROOT/scripts/build_wq_ipa.sh" "$BASE_IPA" "$OUTPUT"
+14
View File
@@ -0,0 +1,14 @@
#!/bin/zsh
# Build both release artifacts:
# build/filza_public.ipa
# build/filza_develop.ipa
set -euo pipefail
ROOT="${0:A:h:h}"
"$ROOT/scripts/build_public.sh" "$ROOT/build/filza_public.ipa"
"$ROOT/scripts/build_developer.sh" "$ROOT/build/filza_develop.ipa"
echo
echo "==> Release artifacts"
ls -lh "$ROOT/build/filza_public.ipa" "$ROOT/build/filza_develop.ipa"
shasum -a 256 "$ROOT/build/filza_public.ipa" "$ROOT/build/filza_develop.ipa"
+91
View File
@@ -0,0 +1,91 @@
#!/bin/zsh
set -euo pipefail
if (( $# < 2 || $# > 3 )); then
echo "usage: $0 <base-unsigned.ipa> <output.ipa> [MCMIdentifiers.plist]" >&2
exit 64
fi
BASE_IPA="${1:A}"
OUTPUT_IPA="${2:A}"
CATALOG="${3:-}"
if [[ -n "$CATALOG" ]]; then
CATALOG="${CATALOG:A}"
fi
REPO_ROOT="${0:A:h:h}"
THEOS="${THEOS:-$HOME/theos}"
export THEOS
FILZASLOP_VERSION="${FILZASLOP_VERSION:-$(
git -C "$REPO_ROOT" describe --tags --exact-match HEAD 2>/dev/null || true
)}"
if [[ -z "$FILZASLOP_VERSION" || ${#FILZASLOP_VERSION} -gt 64 ||
"$FILZASLOP_VERSION" == *[^A-Za-z0-9._-]* ]]; then
echo "invalid FilzaSlop version: $FILZASLOP_VERSION" >&2
echo "tag the release commit or set FILZASLOP_VERSION explicitly" >&2
exit 65
fi
[[ -f "$BASE_IPA" ]] || { echo "base IPA not found: $BASE_IPA" >&2; exit 66; }
if [[ -n "$CATALOG" ]]; then
[[ -f "$CATALOG" ]] || { echo "catalog not found: $CATALOG" >&2; exit 66; }
plutil -lint "$CATALOG" >/dev/null
plutil -extract AppData xml1 -o /dev/null "$CATALOG"
fi
cd "$REPO_ROOT"
make clean
make package FINALPACKAGE=1
DYLIB="$REPO_ROOT/.theos/obj/FilzaApplySandboxExt.dylib"
[[ -f "$DYLIB" ]] || { echo "built dylib not found: $DYLIB" >&2; exit 70; }
STAGE_ROOT="$(mktemp -d /tmp/FilzaSlop-release.XXXXXX)"
trap 'trash "$STAGE_ROOT"' EXIT
unzip -q "$BASE_IPA" -d "$STAGE_ROOT/stage"
APP="$(find "$STAGE_ROOT/stage/Payload" -maxdepth 1 -type d -name '*.app' -print -quit)"
[[ -n "$APP" ]] || { echo "Payload app not found" >&2; exit 65; }
BUNDLE_ID="$(plutil -extract CFBundleIdentifier raw -o - "$APP/Info.plist")"
[[ "$BUNDLE_ID" == "com.apple.mobile.MobileHouseArrest" ]] || {
echo "unexpected bundle identifier: $BUNDLE_ID" >&2
exit 65
}
if codesign -d "$APP" >/dev/null 2>&1; then
echo "base app is signed; use an unsigned base IPA" >&2
exit 65
fi
cp "$DYLIB" "$APP/Frameworks/FilzaApplySandboxExt.dylib"
codesign --remove-signature "$APP/Frameworks/FilzaApplySandboxExt.dylib"
# Strip URL schemes (filza://, Dropbox, Box SDK) — detectable via canOpenURL:
plutil -remove CFBundleURLTypes "$APP/Info.plist" 2>/dev/null || true
if plutil -extract CFBundleURLTypes xml1 -o /dev/null "$APP/Info.plist" \
>/dev/null 2>&1; then
echo "failed to remove CFBundleURLTypes from the release app" >&2
exit 65
fi
plutil -replace FilzaSlopVersion -string "$FILZASLOP_VERSION" \
"$APP/Info.plist" 2>/dev/null ||
plutil -insert FilzaSlopVersion -string "$FILZASLOP_VERSION" "$APP/Info.plist"
if [[ -n "$CATALOG" ]]; then
cp "$CATALOG" "$APP/MCMIdentifiers.plist"
elif [[ -e "$APP/MCMIdentifiers.plist" ]]; then
trash "$APP/MCMIdentifiers.plist"
fi
if [[ -e "$OUTPUT_IPA" ]]; then
trash "$OUTPUT_IPA"
fi
(
cd "$STAGE_ROOT/stage"
zip -qry "$OUTPUT_IPA" Payload
)
shasum -a 256 "$OUTPUT_IPA"
+171
View File
@@ -0,0 +1,171 @@
#!/bin/zsh
# Build the worse_query edition of FilzaSlop and repackage a Filza IPA.
#
# Usage:
# scripts/build_wq_ipa.sh <base-unsigned.ipa> <output.ipa>
#
# Environment:
# BUNDLE_ID Bundle identifier to write into the app (default:
# com.filzaslop.worsequery). It must match the App ID in the
# provisioning profile used for signing.
# WQ_APP_GROUPS Space-separated App Group identifiers. At least one is
# required for worse_query at runtime.
# ENTITLEMENTS Optional entitlements plist. When omitted, the app is
# ad-hoc signed with the groups from WQ_APP_GROUPS.
# SIGN_IDENTITY Optional codesign identity. Omit for an unsigned/ad-hoc
# IPA suitable for third-party signing.
# KEYCHAIN Optional keychain path for SIGN_IDENTITY.
# PROFILE Optional .mobileprovision copied to embedded.mobileprovision.
# FILZASLOP_VERSION Version string shown by the update checker.
set -euo pipefail
if (( $# != 2 )); then
echo "usage: $0 <base-unsigned.ipa> <output.ipa>" >&2
exit 64
fi
BASE_IPA="${1:A}"
OUTPUT_IPA="${2:A}"
REPO_ROOT="${0:A:h:h}"
THEOS="${THEOS:-$HOME/theos}"
BUNDLE_ID="${BUNDLE_ID:-com.jbjf.filza}"
APP_DISPLAY_NAME="${APP_DISPLAY_NAME:-filza27.2}"
WQ_APP_GROUPS="${WQ_APP_GROUPS:-}"
FILZASLOP_VERSION="${FILZASLOP_VERSION:-wq-27.2-1.0}"
SIGN_IDENTITY="${SIGN_IDENTITY:-}"
KEYCHAIN="${KEYCHAIN:-}"
PROFILE="${PROFILE:-}"
ENTITLEMENTS="${ENTITLEMENTS:-}"
[[ -f "$BASE_IPA" ]] || { echo "base IPA not found: $BASE_IPA" >&2; exit 66; }
[[ -d "$THEOS" ]] || { echo "Theos not found: $THEOS" >&2; exit 66; }
if [[ -z "$WQ_APP_GROUPS" ]]; then
echo "WQ_APP_GROUPS is required (space-separated group identifiers)" >&2
exit 65
fi
export THEOS
export PATH="$THEOS/bin:$PATH"
# Theos' bundled ldid may not run on current macOS builds. codesign is enough
# for the intermediate dylib; final app signing happens below.
export TARGET_CODESIGN="${TARGET_CODESIGN:-codesign}"
export TARGET_CODESIGN_FLAGS="${TARGET_CODESIGN_FLAGS:---force --sign -}"
echo "==> Building FilzaApplySandboxExt.dylib"
cd "$REPO_ROOT"
make clean >/dev/null
make FINALPACKAGE=1
DYLIB="$REPO_ROOT/.theos/obj/FilzaApplySandboxExt.dylib"
[[ -f "$DYLIB" ]] || DYLIB="$REPO_ROOT/.theos/obj/debug/FilzaApplySandboxExt.dylib"
[[ -f "$DYLIB" ]] || { echo "built dylib not found" >&2; exit 70; }
STAGE_ROOT="$(mktemp -d /private/tmp/FilzaSlop-WQ.XXXXXX)"
trap 'rm -rf "$STAGE_ROOT"' EXIT
unzip -q "$BASE_IPA" -d "$STAGE_ROOT/stage"
APP="$(find "$STAGE_ROOT/stage/Payload" -maxdepth 1 -type d -name '*.app' -print -quit)"
[[ -n "$APP" ]] || { echo "Payload app not found" >&2; exit 65; }
# The bundled Sharing extension has its own App ID and cannot be authorized by
# the single-App-ID profile used for normal signing.
rm -rf "$APP/PlugIns"
echo "==> Patching Info.plist"
plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP/Info.plist"
plutil -replace CFBundleDisplayName -string "$APP_DISPLAY_NAME" "$APP/Info.plist" 2>/dev/null || \
plutil -insert CFBundleDisplayName -string "$APP_DISPLAY_NAME" "$APP/Info.plist"
plutil -replace CFBundleName -string "$APP_DISPLAY_NAME" "$APP/Info.plist" 2>/dev/null || \
plutil -insert CFBundleName -string "$APP_DISPLAY_NAME" "$APP/Info.plist"
plutil -replace FilzaSlopVersion -string "$FILZASLOP_VERSION" "$APP/Info.plist" 2>/dev/null || \
plutil -insert FilzaSlopVersion -string "$FILZASLOP_VERSION" "$APP/Info.plist"
plutil -remove CFBundleURLTypes "$APP/Info.plist" 2>/dev/null || true
plutil -insert NSLocalNetworkUsageDescription -string \
"FilzaSlop runs a local WebDAV server so you can browse device storage from a computer." \
"$APP/Info.plist" 2>/dev/null || true
plutil -insert NSBonjourServices -json '["_http._tcp.","_webdav._tcp."]' \
"$APP/Info.plist" 2>/dev/null || true
# Runtime-readable copy of the groups (SecTask is the primary source).
groups_json="["
first=1
for group in ${(s: :)WQ_APP_GROUPS}; do
[[ -n "$group" ]] || continue
if (( first == 0 )); then groups_json+=","; fi
groups_json+="\"$group\""
first=0
done
groups_json+="]"
plutil -remove WQAppGroups "$APP/Info.plist" 2>/dev/null || true
plutil -insert WQAppGroups -json "$groups_json" "$APP/Info.plist"
echo "==> Adding bundled test files"
mkdir -p "$APP/WQTestFiles"
if [[ -f "$REPO_ROOT/tests/test.deb" ]]; then
cp "$REPO_ROOT/tests/test.deb" "$APP/WQTestFiles/test.deb"
fi
if [[ -f "$REPO_ROOT/tests/test" ]]; then
cp "$REPO_ROOT/tests/test" "$APP/WQTestFiles/test"
chmod 0755 "$APP/WQTestFiles/test"
fi
echo "==> Injecting dylib"
mkdir -p "$APP/Frameworks"
cp "$DYLIB" "$APP/Frameworks/FilzaApplySandboxExt.dylib"
codesign --remove-signature "$APP/Frameworks/FilzaApplySandboxExt.dylib" 2>/dev/null || true
if [[ "${WQ_TERMINAL:-0}" == "full" ]]; then
ASHELL_IPA="${ASHELL_IPA:-$HOME/Downloads/Terminal.ipa}"
"$REPO_ROOT/scripts/prepare_dev_terminal.sh" "$APP" "$ASHELL_IPA"
cp "$REPO_ROOT/vendor/libssh/libssh.4.dylib" "$APP/Frameworks/libssh.4.dylib"
elif [[ "${WQ_TERMINAL:-0}" == "1" ]]; then
ASHELL_IPA="${ASHELL_IPA:-$HOME/Downloads/Terminal.ipa}"
"$REPO_ROOT/scripts/prepare_mini_terminal.sh" "$APP" "$ASHELL_IPA"
fi
if [[ -n "$PROFILE" ]]; then
[[ -f "$PROFILE" ]] || { echo "profile not found: $PROFILE" >&2; exit 66; }
cp "$PROFILE" "$APP/embedded.mobileprovision"
fi
if [[ -n "$ENTITLEMENTS" ]]; then
[[ -f "$ENTITLEMENTS" ]] || { echo "entitlements not found: $ENTITLEMENTS" >&2; exit 66; }
plutil -lint "$ENTITLEMENTS" >/dev/null
else
ENTITLEMENTS="$STAGE_ROOT/wq-entitlements.plist"
{
echo '<?xml version="1.0" encoding="UTF-8"?>'
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
echo '<plist version="1.0"><dict>'
echo '<key>com.apple.security.application-groups</key><array>'
for group in ${(s: :)WQ_APP_GROUPS}; do
[[ -n "$group" ]] && echo "<string>$group</string>"
done
echo '</array></dict></plist>'
} > "$ENTITLEMENTS"
fi
echo "==> Signing nested code"
sign_args=(--force --timestamp=none)
if [[ -n "$SIGN_IDENTITY" ]]; then
sign_args+=(--sign "$SIGN_IDENTITY")
[[ -n "$KEYCHAIN" ]] && sign_args+=(--keychain "$KEYCHAIN")
else
sign_args+=(--sign -)
fi
while IFS= read -r -d '' candidate; do
if file -b "$candidate" | grep -q 'Mach-O'; then
codesign "${sign_args[@]}" "$candidate"
fi
done < <(find "$APP" -type f -perm +111 -print0)
echo "==> Signing app"
codesign "${sign_args[@]}" --entitlements "$ENTITLEMENTS" "$APP"
codesign --verify --deep --strict "$APP"
rm -f "$OUTPUT_IPA"
(
cd "$STAGE_ROOT/stage"
zip -qry "$OUTPUT_IPA" Payload
)
echo "==> Wrote $OUTPUT_IPA"
shasum -a 256 "$OUTPUT_IPA"
+232
View File
@@ -0,0 +1,232 @@
#!/bin/zsh
# Build the larger developer terminal payload: every a-Shell command except
# Python, Node.js, Lua/TeX (and commands that hard-depend on Python, such as
# vim and xxd). Resources for Perl/man/ImageMagick are kept.
#
# Usage: prepare_dev_terminal.sh <app-path> [ashell-ipa]
set -euo pipefail
APP="${1:A}"
ASHELL_IPA="${2:-$HOME/Downloads/Terminal.ipa}"
REPO_ROOT="${0:A:h:h}"
RES_SRC="$REPO_ROOT/../a-shell-master/Resources_mini"
[[ -d "$APP" ]] || { echo "app not found: $APP" >&2; exit 66; }
[[ -f "$ASHELL_IPA" ]] || { echo "a-Shell IPA not found: $ASHELL_IPA" >&2; exit 66; }
[[ -d "$RES_SRC" ]] || { echo "Resources_mini not found: $RES_SRC" >&2; exit 66; }
python3 - "$APP" "$ASHELL_IPA" "$RES_SRC" <<'PY'
import os, re, shutil, struct, sys, zipfile, plistlib
app, ipa, resources_mini = sys.argv[1], sys.argv[2], sys.argv[3]
z = zipfile.ZipFile(ipa)
names = z.namelist()
app_prefix = None
for n in names:
m = re.match(r'(Payload/[^/]+\.app/)', n)
if m:
app_prefix = m.group(1)
break
if not app_prefix:
raise SystemExit('cannot find app prefix in IPA')
print('IPA app:', app_prefix)
def exclude_framework(name):
n = name.lower().replace('.framework', '')
if n.startswith('python') or n.startswith('python'):
return 'Python'
if n in ('node', 'npm', 'node-fetch'):
return 'Node.js'
if n in ('vim', 'xxd'):
return 'Python dependency'
tex_names = ('xetex', 'luatex', 'luahbtex', 'pdftex', 'euptex',
'texlua53', 'texluajit', 'kpathsea', 'kpsewhich', 'bibtex',
'metapost', 'mflua', 'mplib', 'mf')
if n in tex_names or n.startswith('lua') or (
n.startswith('tex') and n != 'text'):
return 'TeX/Lua'
if n in ('libllvm', 'clang', 'lld', 'ar'):
return 'unused compiler'
if n in ('libgdal', 'openblas', 'libgfortran', 'libproj', 'libgeos',
'libgeos_c', 'libspatialindex', 'libspatialindex_c'):
return 'Python geo/math'
# Duplicate variant frameworks (A/B/C/D/E) are runtime alternatives.
if re.search(r'(dash|perl|ssh_cmd|openrsync|xetex|luahbtex|luatex|pdftex|euptex)[A-E]$', n):
return 'duplicate variant'
return None
fw_size = {}
fw_files = {}
for n in names:
if not n.startswith(app_prefix + 'Frameworks/'):
continue
rest = n[len(app_prefix + 'Frameworks/'):]
parts = rest.split('/')
if len(parts) < 2 or not parts[0].endswith('.framework'):
continue
fw = parts[0]
fw_size[fw] = fw_size.get(fw, 0) + z.getinfo(n).file_size
fw_files.setdefault(fw, []).append(n)
def macho_deps(data):
out = []
if len(data) < 32:
return out
magic = struct.unpack_from('<I', data, 0)[0]
if magic in (0xfeedfacf, 0xfeedface):
ncmds = struct.unpack_from('<I', data, 16)[0]
off = 32 if magic == 0xfeedfacf else 28
for _ in range(ncmds):
if off + 8 > len(data):
break
cmd, size = struct.unpack_from('<II', data, off)
if size < 8 or off + size > len(data):
break
if (cmd & 0x7fffffff) in (0xc, 0x18, 0x1f, 0x23):
nameoff = struct.unpack_from('<I', data, off + 8)[0]
start = off + nameoff
end = data.find(b'\0', start, off + size)
if end > start:
out.append(data[start:end].decode('utf-8', 'replace'))
off += size
elif magic in (0xcafebabe, 0xcafebabf):
n = struct.unpack_from('>I', data, 4)[0]
for i in range(n):
off = (struct.unpack_from('>I', data, 8 + i*20 + 8)[0]
if magic == 0xcafebabe else
struct.unpack_from('>Q', data, 8 + i*32 + 8)[0])
out += macho_deps(data[off:])
return out
deps = {}
for fw, files in fw_files.items():
base = fw[:-len('.framework')]
target = next((f for f in files if os.path.basename(f) == base), None)
if target is None:
continue
try:
for d in macho_deps(z.read(target)):
if '.framework/' in d:
deps.setdefault(fw, set()).add(d.split('/')[-2].replace('.framework', '') + '.framework')
except Exception:
pass
cd = plistlib.loads(z.read(app_prefix + 'commandDictionary.plist'))
selected = set()
for cmd, info in cd.items():
if not isinstance(info, list) or len(info) < 4:
continue
path = info[0]
if path == 'SELF':
continue
fw = path.split('/')[0]
if not fw.endswith('.framework'):
fw += '.framework'
if fw in fw_size and not exclude_framework(fw):
selected.add(fw)
# add transitive dependencies
pending = list(selected)
while pending:
fw = pending.pop()
for d in deps.get(fw, ()):
if d in fw_size and d not in selected and not exclude_framework(d):
selected.add(d)
pending.append(d)
print('selected frameworks:', len(selected), 'size:', round(sum(fw_size[f] for f in selected)/1e6, 1), 'MB')
# extract frameworks
os.makedirs(os.path.join(app, 'Frameworks'), exist_ok=True)
for fw in sorted(selected):
dest = os.path.join(app, 'Frameworks', fw)
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest, exist_ok=True)
for n in fw_files[fw]:
if '/_CodeSignature/' in n:
continue
rel = n[len(app_prefix + 'Frameworks/' + fw + '/'):]
if not rel:
continue
out = os.path.join(dest, rel)
if n.endswith('/'):
os.makedirs(out, exist_ok=True)
continue
os.makedirs(os.path.dirname(out), exist_ok=True)
info = z.getinfo(n)
with z.open(n) as src, open(out, 'wb') as dst:
shutil.copyfileobj(src, dst)
mode = (info.external_attr >> 16) & 0o777
if mode:
os.chmod(out, mode)
# resources
wqres = os.path.join(app, 'WQResources')
shutil.rmtree(wqres, ignore_errors=True)
os.makedirs(wqres, exist_ok=True)
for sub in ('bin', 'usr'):
src = os.path.join(resources_mini, sub)
if os.path.isdir(src):
shutil.copytree(src, os.path.join(wqres, sub), symlinks=True)
# top-level a-Shell resources that are not Python/Node/TeX
for sub in ('Perl', 'man', 'task', 'terminfo', 'ImageMagick-7'):
dest = os.path.join(wqres, sub)
found = False
for n in names:
if n.startswith(app_prefix + sub + '/'):
if not found:
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest, exist_ok=True)
found = True
rel = n[len(app_prefix + sub + '/'):]
if not rel:
continue
out = os.path.join(dest, rel)
if n.endswith('/'):
os.makedirs(out, exist_ok=True)
continue
os.makedirs(os.path.dirname(out), exist_ok=True)
info = z.getinfo(n)
with z.open(n) as src, open(out, 'wb') as dst:
shutil.copyfileobj(src, dst)
mode = (info.external_attr >> 16) & 0o777
if mode:
os.chmod(out, mode)
if found:
print('resource:', sub)
for name in ('cacert.pem',):
n = app_prefix + name
if n in z.NameToInfo:
with z.open(n) as src, open(os.path.join(wqres, name), 'wb') as dst:
shutil.copyfileobj(src, dst)
# filtered command dictionary
out_cd = {}
for cmd, info in cd.items():
if not isinstance(info, list) or len(info) < 4:
continue
path = info[0]
if path == 'SELF':
out_cd[cmd] = info
continue
fw = path.split('/')[0]
if not fw.endswith('.framework'):
fw += '.framework'
if fw in selected:
out_cd[cmd] = info
plistlib.dump(out_cd, open(os.path.join(app, 'commandDictionary.plist'), 'wb'))
print('commands kept:', len(out_cd), '/', len(cd))
# Remove zero-byte placeholders for excluded commands from WQResources/bin.
bin_dir = os.path.join(wqres, 'bin')
if os.path.isdir(bin_dir):
for name in os.listdir(bin_dir):
p = os.path.join(bin_dir, name)
if not os.path.isfile(p) or os.path.getsize(p) != 0:
continue
if name not in out_cd:
os.unlink(p)
print('done')
PY
+67
View File
@@ -0,0 +1,67 @@
#!/bin/zsh
# Copy the a-Shell mini ios_system runtime into a Filza app bundle.
# Usage: prepare_mini_terminal.sh <app-path> [ashell-ipa]
set -euo pipefail
APP="${1:A}"
ASHELL_IPA="${2:-$HOME/Downloads/Terminal.ipa}"
FRAMEWORKS=(
ios_system shell files text network_ios dash tar awk curl_ios jq bc_ios
calc xxd unrar openrsync mandoc task lua_ios lex lg2 multimarkdown pico
blink ssh_cmd ssh_agent vim
)
[[ -d "$APP" ]] || { echo "app not found: $APP" >&2; exit 66; }
[[ -f "$ASHELL_IPA" ]] || { echo "a-Shell IPA not found: $ASHELL_IPA" >&2; exit 66; }
TMP="$(mktemp -d /private/tmp/wq-mini.XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
unzip -Z1 "$ASHELL_IPA" > "$TMP/list"
ASHELL_APPDIR="$(grep -m1 -E '^Payload/[^/]+\.app/' "$TMP/list" | cut -d/ -f1-2)"
[[ -n "$ASHELL_APPDIR" ]] || { echo "cannot locate app inside IPA" >&2; exit 65; }
echo "==> Extracting mini command frameworks"
mkdir -p "$APP/Frameworks"
for framework in "${FRAMEWORKS[@]}"; do
archive_path="${ASHELL_APPDIR}/Frameworks/${framework}.framework"
if ! unzip -q "$ASHELL_IPA" "${archive_path}/*" -d "$TMP/extract" 2>/dev/null; then
echo " skip missing ${framework}.framework"
continue
fi
rm -rf "$APP/Frameworks/${framework}.framework"
cp -R "$TMP/extract/${archive_path}" "$APP/Frameworks/${framework}.framework"
rm -rf "$APP/Frameworks/${framework}.framework/_CodeSignature"
echo " ${framework}.framework"
done
echo "==> Copying mini resources"
RES_SRC="${ASHELL_RESOURCES:-$(dirname "$0")/../Resources_mini}"
if [[ ! -d "$RES_SRC" ]]; then
RES_SRC="$(dirname "$0")/../../a-shell-master/Resources_mini"
fi
[[ -d "$RES_SRC" ]] || { echo "Resources_mini not found" >&2; exit 66; }
rm -rf "$APP/WQResources"
mkdir -p "$APP/WQResources"
cp -R "$RES_SRC/bin" "$APP/WQResources/bin"
cp -R "$RES_SRC/usr" "$APP/WQResources/usr"
chmod -R u+rwX "$APP/WQResources"
echo "==> Filtering command dictionary"
python3 - "$RES_SRC/commandDictionary.plist" "$APP/commandDictionary.plist" "${FRAMEWORKS[@]}" <<'PY'
import plistlib, sys
source, destination = sys.argv[1], sys.argv[2]
selected = set(sys.argv[3:])
d = plistlib.load(open(source, 'rb'))
out = {}
for command, info in d.items():
if not isinstance(info, list) or len(info) < 4:
continue
path = info[0]
framework = path.split('/')[0].replace('.framework', '')
if path == 'SELF' or framework in selected:
out[command] = info
plistlib.dump(out, open(destination, 'wb'))
print(f"commands: {len(out)} / {len(d)}")
PY
echo "==> Mini terminal payload ready: $(du -sh "$APP/Frameworks" "$APP/WQResources" | tr '\n' ' ')"
+50
View File
@@ -0,0 +1,50 @@
#!/bin/zsh
set -euo pipefail
if (( $# < 1 || $# > 2 )); then
echo "usage: $0 <device-udid> [output.plist]" >&2
exit 64
fi
UDID="$1"
OUTPUT="${2:-/tmp/MCMIdentifiers.plist}"
PYMOBILEDEVICE3="${PYMOBILEDEVICE3:-pymobiledevice3}"
TEMP_JSON="$(mktemp /tmp/filza-mcm-apps.XXXXXX.json)"
trap 'trash "$TEMP_JSON"' EXIT
"$PYMOBILEDEVICE3" apps list --type Any --udid "$UDID" > "$TEMP_JSON"
python3 - "$TEMP_JSON" "$OUTPUT" <<'PY'
import json
import plistlib
import re
import sys
source, output = sys.argv[1:]
records = json.load(open(source, "r", encoding="utf-8"))
safe = re.compile(r"^[A-Za-z0-9._-]{1,255}$")
apps = set()
groups = set()
for map_key, record in records.items():
if not isinstance(record, dict):
continue
identifier = record.get("CFBundleIdentifier") or map_key
if isinstance(identifier, str) and safe.fullmatch(identifier):
apps.add(identifier)
containers = record.get("GroupContainers")
if isinstance(containers, dict):
groups.update(key for key in containers if isinstance(key, str) and safe.fullmatch(key))
catalog = {
"AppData": sorted(apps),
"AppGroups": sorted(groups),
"ExtensionData": [],
"VPNData": [],
"ServiceData": [],
"SystemData": [],
"SystemGroups": [],
"ProtectedData": [],
}
with open(output, "wb") as stream:
plistlib.dump(catalog, stream, sort_keys=False)
print(f"wrote {output}: app_data={len(apps)} app_groups={len(groups)}")
PY