update 2026-10-07 11:52:44

This commit is contained in:
action committed 2026-10-07 11:52:44 +08:00
1 parent d6cc6efa77
commit 617f7ca2a6
4898 files changed
-1713675

No files matched your search

@@ -1,270 +0,0 @@
#!/bin/sh
set -eu
VERSION="${1:-0.1.0}"
OUT_DIR="${2:-dist}"
PKG_VERSION="$(printf '%s' "$VERSION" | sed 's/^v//')"
PKG_NAME="luci-app-oxidns"
PKG_FILE_BASE="${PKG_NAME}_${PKG_VERSION}-r1_all"
I18N_PKG_NAME="luci-i18n-oxidns-zh-cn"
I18N_FILE_BASE="${I18N_PKG_NAME}_${PKG_VERSION}-r1_all"
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'required command not found: %s\n' "$1" >&2
exit 1
}
}
need_cmd tar
need_cmd gzip
need_cmd node
need_cmd sha256sum
export COPYFILE_DISABLE=1
tar_create_gz() {
tar_gz_out="$1"
shift
tar --format=ustar --owner=0 --group=0 --numeric-owner -czf "$tar_gz_out" "$@"
}
write_file_list() {
file_list_dir="$1"
file_list_out="$2"
(
cd "$file_list_dir"
find . ! -name . | sed 's#^\./##' | LC_ALL=C sort
) > "$file_list_out"
}
tar_create_segment_gz_from_list() {
tar_segment_out="$1"
tar_segment_dir="$2"
tar_segment_list="$3"
tar_segment_raw="${tar_segment_out%.gz}"
tar_segment_cut="$tar_segment_raw.cut"
tar --format=ustar --owner=0 --group=0 --numeric-owner -cf "$tar_segment_raw" -C "$tar_segment_dir" -T "$tar_segment_list"
node scripts/strip-tar-eof.mjs "$tar_segment_raw" "$tar_segment_cut"
gzip -9n < "$tar_segment_cut" > "$tar_segment_out"
}
installed_size() {
find "$1" -type f -exec wc -c {} + | awk 'END { print $1 + 0 }'
}
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/luci-app-oxidns-build.XXXXXX")"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT HUP INT TERM
create_ipk() {
ipk_out="$1"
ipk_control_tar="$2"
ipk_data_tar="$3"
ipk_dir="$TMP_DIR/ipk-$(basename "$ipk_out" .ipk)"
rm -rf "$ipk_dir"
mkdir -p "$ipk_dir"
cp "$ipk_data_tar" "$ipk_dir/data.tar.gz"
cp "$ipk_control_tar" "$ipk_dir/control.tar.gz"
tar_create_gz "$ipk_out" -C "$ipk_dir" control.tar.gz data.tar.gz
}
create_apk() {
apk_out="$1"
apk_control_dir="$2"
apk_data_dir="$3"
apk_name="$(basename "$apk_out" .apk)"
apk_control_list="$TMP_DIR/$apk_name.control.list"
apk_data_raw="$TMP_DIR/$apk_name.data.tar"
apk_data_tar="$TMP_DIR/$apk_name.data.tar.gz"
apk_control_tar="$TMP_DIR/$apk_name.control.tar.gz"
apk_datahash=""
node scripts/write-apk-data-tar.mjs "$apk_data_dir" "$apk_data_raw"
gzip -9n < "$apk_data_raw" > "$apk_data_tar"
apk_datahash="$(sha256sum "$apk_data_tar" | awk '{ print $1 }')"
printf 'datahash = %s\n' "$apk_datahash" >> "$apk_control_dir/.PKGINFO"
{
printf '.PKGINFO\n'
(
cd "$apk_control_dir"
find . ! -name . ! -name .PKGINFO | sed 's#^\./##' | LC_ALL=C sort
)
} > "$apk_control_list"
tar_create_segment_gz_from_list "$apk_control_tar" "$apk_control_dir" "$apk_control_list"
cat "$apk_control_tar" "$apk_data_tar" > "$apk_out"
}
write_rpcd_restart_script() {
out="$1"
cat > "$out" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT:-}" ] && exit 0
rm -f /tmp/luci-indexcache* 2>/dev/null || true
rm -rf /tmp/luci-modulecache/* 2>/dev/null || true
if [ -d /www/luci-static/resources/view/oxidns ]; then
find /www/luci-static/resources/view/oxidns -type f -name '*.js' -exec touch {} + 2>/dev/null || true
fi
if [ -x /etc/init.d/rpcd ]; then
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
fi
exit 0
EOF
chmod 755 "$out"
}
write_i18n_postinst_script() {
out="$1"
cat > "$out" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT:-}" ] && exit 0
if [ -x /etc/uci-defaults/luci-i18n-oxidns-zh-cn ]; then
/etc/uci-defaults/luci-i18n-oxidns-zh-cn >/dev/null 2>&1 || true
fi
rm -f /tmp/luci-indexcache* 2>/dev/null || true
rm -rf /tmp/luci-modulecache/* 2>/dev/null || true
if [ -x /etc/init.d/rpcd ]; then
/etc/init.d/rpcd restart >/dev/null 2>&1 || true
fi
exit 0
EOF
chmod 755 "$out"
}
CONTROL_DIR="$TMP_DIR/control"
DATA_DIR="$TMP_DIR/data"
APK_CONTROL_DIR="$TMP_DIR/apk-control"
I18N_CONTROL_DIR="$TMP_DIR/i18n-control"
I18N_DATA_DIR="$TMP_DIR/i18n-data"
I18N_APK_CONTROL_DIR="$TMP_DIR/i18n-apk-control"
mkdir -p "$CONTROL_DIR" "$DATA_DIR" "$APK_CONTROL_DIR" "$OUT_DIR"
OUT_DIR="$(cd "$OUT_DIR" && pwd)"
cat > "$CONTROL_DIR/control" <<EOF
Package: $PKG_NAME
Version: $PKG_VERSION-r1
Architecture: all
Maintainer: Sven Shi <isvenshi@gmail.com>
Depends: luci-base, rpcd, jsonfilter, uclient-fetch, ca-bundle
Source: https://github.com/svenshi/luci-app-oxidns
Section: luci
Priority: optional
Description: LuCI support for OxiDNS
EOF
mkdir -p "$DATA_DIR/www" "$DATA_DIR"
if [ -d htdocs ]; then
cp -R htdocs/. "$DATA_DIR/www/"
fi
if [ -d root ]; then
cp -R root/. "$DATA_DIR/"
fi
chmod 755 "$DATA_DIR/usr/libexec/rpcd/luci.oxidns"
if [ -f "$DATA_DIR/etc/init.d/oxidns" ]; then
chmod 755 "$DATA_DIR/etc/init.d/oxidns"
fi
write_rpcd_restart_script "$CONTROL_DIR/postinst"
write_rpcd_restart_script "$CONTROL_DIR/postrm"
printf '2.0\n' > "$TMP_DIR/debian-binary"
tar_create_gz "$TMP_DIR/control.tar.gz" -C "$CONTROL_DIR" .
tar_create_gz "$TMP_DIR/data.tar.gz" -C "$DATA_DIR" .
create_ipk "$OUT_DIR/${PKG_FILE_BASE}.ipk" "$TMP_DIR/control.tar.gz" "$TMP_DIR/data.tar.gz"
cat > "$APK_CONTROL_DIR/.PKGINFO" <<EOF
pkgname = $PKG_NAME
pkgver = $PKG_VERSION-r1
pkgdesc = LuCI support for OxiDNS
url = https://github.com/svenshi/luci-app-oxidns
builddate = $(date +%s)
packager = Sven Shi <isvenshi@gmail.com>
size = $(installed_size "$DATA_DIR")
arch = noarch
origin = $PKG_NAME
license = GPL-3.0-or-later
depend = luci-base
depend = rpcd
depend = jsonfilter
depend = uclient-fetch
depend = ca-bundle
EOF
write_rpcd_restart_script "$APK_CONTROL_DIR/.post-install"
cp "$APK_CONTROL_DIR/.post-install" "$APK_CONTROL_DIR/.post-upgrade"
cp "$APK_CONTROL_DIR/.post-install" "$APK_CONTROL_DIR/.post-deinstall"
create_apk "$OUT_DIR/${PKG_FILE_BASE}.apk" "$APK_CONTROL_DIR" "$DATA_DIR"
printf 'Wrote %s\n' "$OUT_DIR/${PKG_FILE_BASE}.ipk"
printf 'Wrote %s\n' "$OUT_DIR/${PKG_FILE_BASE}.apk"
if [ -f po/zh_Hans/oxidns.po ]; then
mkdir -p "$I18N_CONTROL_DIR" "$I18N_APK_CONTROL_DIR" "$I18N_DATA_DIR/usr/lib/lua/luci/i18n" "$I18N_DATA_DIR/etc/uci-defaults"
cat > "$I18N_CONTROL_DIR/control" <<-EOF
Package: $I18N_PKG_NAME
Version: $PKG_VERSION-r1
Architecture: all
Maintainer: Sven Shi <isvenshi@gmail.com>
Depends: $PKG_NAME
Source: https://github.com/svenshi/luci-app-oxidns
Section: luci
Priority: optional
Description: Simplified Chinese translation for luci-app-oxidns
EOF
cat > "$I18N_DATA_DIR/etc/uci-defaults/$I18N_PKG_NAME" <<-'EOF'
uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'
uci commit luci
EOF
chmod 755 "$I18N_DATA_DIR/etc/uci-defaults/$I18N_PKG_NAME"
write_i18n_postinst_script "$I18N_CONTROL_DIR/postinst"
node scripts/po2lmo.mjs po/zh_Hans/oxidns.po "$I18N_DATA_DIR/usr/lib/lua/luci/i18n/oxidns.zh-cn.lmo"
tar_create_gz "$TMP_DIR/control.tar.gz" -C "$I18N_CONTROL_DIR" .
tar_create_gz "$TMP_DIR/data.tar.gz" -C "$I18N_DATA_DIR" .
create_ipk "$OUT_DIR/${I18N_FILE_BASE}.ipk" "$TMP_DIR/control.tar.gz" "$TMP_DIR/data.tar.gz"
cat > "$I18N_APK_CONTROL_DIR/.PKGINFO" <<-EOF
pkgname = $I18N_PKG_NAME
pkgver = $PKG_VERSION-r1
pkgdesc = Simplified Chinese translation for luci-app-oxidns
url = https://github.com/svenshi/luci-app-oxidns
builddate = $(date +%s)
packager = Sven Shi <isvenshi@gmail.com>
size = $(installed_size "$I18N_DATA_DIR")
arch = noarch
origin = $I18N_PKG_NAME
license = GPL-3.0-or-later
depend = $PKG_NAME
EOF
write_i18n_postinst_script "$I18N_APK_CONTROL_DIR/.post-install"
cp "$I18N_APK_CONTROL_DIR/.post-install" "$I18N_APK_CONTROL_DIR/.post-upgrade"
create_apk "$OUT_DIR/${I18N_FILE_BASE}.apk" "$I18N_APK_CONTROL_DIR" "$I18N_DATA_DIR"
(
cd "$OUT_DIR"
sha256sum \
"${PKG_FILE_BASE}.ipk" \
"${PKG_FILE_BASE}.apk" \
"${I18N_FILE_BASE}.ipk" \
"${I18N_FILE_BASE}.apk"
) > "$OUT_DIR/sha256sums.txt"
printf 'Wrote %s\n' "$OUT_DIR/${I18N_FILE_BASE}.ipk"
printf 'Wrote %s\n' "$OUT_DIR/${I18N_FILE_BASE}.apk"
else
(
cd "$OUT_DIR"
sha256sum "${PKG_FILE_BASE}.ipk" "${PKG_FILE_BASE}.apk"
) > "$OUT_DIR/sha256sums.txt"
fi
-75
View File
@@ -1,75 +0,0 @@
#!/bin/sh
set -eu
node -e "for (const f of ['htdocs/luci-static/resources/view/oxidns/overview.js','htdocs/luci-static/resources/view/oxidns/core.js','htdocs/luci-static/resources/view/oxidns/config.js','htdocs/luci-static/resources/view/oxidns/logs.js','htdocs/luci-static/resources/view/oxidns/settings.js']) new Function(require('fs').readFileSync(f,'utf8'));"
node -e "for (const f of ['root/usr/share/luci/menu.d/luci-app-oxidns.json','root/usr/share/rpcd/acl.d/luci-app-oxidns.json','root/usr/share/oxidns/targets.json']) JSON.parse(require('fs').readFileSync(f,'utf8'));"
node <<'NODE'
const fs = require('fs');
const required = new Set();
for (const file of [
'htdocs/luci-static/resources/view/oxidns/overview.js',
'htdocs/luci-static/resources/view/oxidns/core.js',
'htdocs/luci-static/resources/view/oxidns/config.js',
'htdocs/luci-static/resources/view/oxidns/logs.js',
'htdocs/luci-static/resources/view/oxidns/settings.js',
]) {
const source = fs.readFileSync(file, 'utf8');
const re = /_\(\s*(['"])((?:\\.|[^\\])*?)\1\s*\)/g;
let match;
while ((match = re.exec(source)))
required.add(Function(`return ${match[1]}${match[2]}${match[1]}`)());
}
const menu = JSON.parse(fs.readFileSync('root/usr/share/luci/menu.d/luci-app-oxidns.json', 'utf8'));
for (const entry of Object.values(menu)) {
if (entry.title)
required.add(entry.title);
}
function poIds(file) {
const ids = new Set();
const content = fs.readFileSync(file, 'utf8');
const re = /^msgid "((?:\\.|[^"\\])*)"$/mg;
let match;
while ((match = re.exec(content))) {
const id = JSON.parse(`"${match[1]}"`);
if (id)
ids.add(id);
}
return ids;
}
const pot = poIds('po/templates/oxidns.pot');
const zh = poIds('po/zh_Hans/oxidns.po');
const failures = [];
for (const [label, ids] of [['POT', pot], ['zh_Hans PO', zh]]) {
for (const id of [...required].sort()) {
if (!ids.has(id))
failures.push(`${label} missing msgid: ${id}`);
}
}
for (const id of [...pot].sort()) {
if (!required.has(id))
failures.push(`POT stale msgid: ${id}`);
}
if (failures.length) {
console.error(failures.join('\n'));
process.exit(1);
}
NODE
for script in scripts/po2lmo.mjs scripts/strip-tar-eof.mjs scripts/write-apk-data-tar.mjs scripts/write-ar-archive.mjs; do
node --check "$script"
done
node scripts/po2lmo.mjs po/zh_Hans/oxidns.po "${TMPDIR:-/tmp}/oxidns.zh-cn.lmo"
test -s "${TMPDIR:-/tmp}/oxidns.zh-cn.lmo"
rm -f "${TMPDIR:-/tmp}/oxidns.zh-cn.lmo"
if command -v msgfmt >/dev/null 2>&1; then
msgfmt --check po/zh_Hans/oxidns.po -o /dev/null
fi
sh -n root/usr/libexec/rpcd/luci.oxidns
sh -n root/etc/init.d/oxidns
sh -n scripts/build-luci-package.sh
sh -n scripts/integration-check.sh
sh -n scripts/release-check.sh
@@ -1,252 +0,0 @@
#!/bin/sh
set -eu
json_ok() {
node -e "const v=JSON.parse(require('fs').readFileSync(0,'utf8')); if (!($1)) process.exit(1);"
}
tar_has_member() {
tar -tzf "$1" | awk -v member="$2" '
{
path = $0;
sub(/^\.\//, "", path);
sub(/\/$/, "", path);
if (path == member)
found = 1;
}
END { exit found ? 0 : 1 }
'
}
tar_member_contains() {
tar -xOzf "$1" "$2" 2>/dev/null | grep -q "$3"
}
apk_data_has_checksum() {
gzip -dc "$1" | grep -q 'APK-TOOLS.checksum.SHA1='
}
tar_nested_has_member() {
outer="$1"
inner="$2"
member="$3"
nested="$(mktemp "${TMPDIR:-/tmp}/luci-app-oxidns-nested.XXXXXX")"
if ! tar -xOf "$outer" "$inner" > "$nested" 2>/dev/null &&
! tar -xOf "$outer" "./$inner" > "$nested" 2>/dev/null; then
rm -f "$nested"
return 1
fi
if tar -tzf "$nested" | awk -v member="$member" '
{
path = $0;
sub(/^\.\//, "", path);
if (path == member)
found = 1;
}
END { exit found ? 0 : 1 }
'; then
rm -f "$nested"
return 0
fi
rm -f "$nested"
return 1
}
assert_unsafe_upload_rejected() {
upload="$1"
printf '{"path":"%s"}' "$upload" |
root/usr/libexec/rpcd/luci.oxidns call core_upload_install |
json_ok "v.ok === false && v.code === 'uploaded_archive_unsafe'"
}
scripts/check.sh
root/usr/libexec/rpcd/luci.oxidns list | json_ok "'status' in v && 'core_install' in v && 'core_reinstall' in v && 'core_upload_install' in v && 'core_progress' in v && 'core_remove' in v && 'logs_recent' in v && 'settings_read' in v && !('config_basic_read' in v) && !('config_basic_save' in v)"
root/usr/libexec/rpcd/luci.oxidns call status | json_ok "v.ok === true && v.core && v.core.installed === false && v.webui && v.webui.installed === false && typeof v.webui.url === 'string' && typeof v.webui.local_only === 'boolean' && typeof v.webui.wildcard === 'boolean' && !('api' in v) && !('api_base_url' in v) && !('package' in v) && !('package_manager' in v)"
TARGET_FAKE_BIN="$(mktemp -d "${TMPDIR:-/tmp}/oxidns-target-fake-bin.XXXXXX")"
TARGET_CASES="$(mktemp "${TMPDIR:-/tmp}/oxidns-target-cases.XXXXXX")"
cat > "$TARGET_FAKE_BIN/uname" <<'EOF'
#!/bin/sh
if [ "${1:-}" = "-m" ]; then
printf '%s\n' "${FAKE_UNAME_M:?}"
exit 0
fi
exec /usr/bin/uname "$@"
EOF
chmod 755 "$TARGET_FAKE_BIN/uname"
node -e "const data=JSON.parse(require('fs').readFileSync('root/usr/share/oxidns/targets.json','utf8')); for (const target of data.targets) for (const arch of target.uname_arches) console.log(arch + ' ' + target.rust_target);" > "$TARGET_CASES"
while read -r TARGET_ARCH TARGET_RUST; do
PATH="$TARGET_FAKE_BIN:$PATH" FAKE_UNAME_M="$TARGET_ARCH" OXIDNS_TARGETS_FILE="$PWD/root/usr/share/oxidns/targets.json" \
root/usr/libexec/rpcd/luci.oxidns call status |
json_ok "v.ok === true && v.core && v.core.target === '$TARGET_RUST'"
done < "$TARGET_CASES"
rm -rf "$TARGET_FAKE_BIN"
rm -f "$TARGET_CASES"
root/usr/libexec/rpcd/luci.oxidns call core_progress | json_ok "v.ok === true && typeof v.text === 'string'"
root/usr/libexec/rpcd/luci.oxidns call core_reinstall | json_ok "v.ok === false && v.code === 'core_not_installed'"
printf '%s' '{"path":"/etc/passwd"}' | root/usr/libexec/rpcd/luci.oxidns call core_upload_install | json_ok "v.ok === false && v.code === 'invalid_upload_path'"
LOCK_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/oxidns-lock-test.XXXXXX")"
mkdir -p "$LOCK_TEST_DIR/luci-oxidns/core-operation.lock"
printf '%s\n' "$$" > "$LOCK_TEST_DIR/luci-oxidns/core-operation.lock/pid"
TMPDIR="$LOCK_TEST_DIR" root/usr/libexec/rpcd/luci.oxidns call core_reinstall | json_ok "v.ok === false && v.code === 'core_operation_busy'"
rm -rf "$LOCK_TEST_DIR"
node -e "const acl=JSON.parse(require('fs').readFileSync('root/usr/share/rpcd/acl.d/luci-app-oxidns.json','utf8'))['luci-app-oxidns']; const read=acl.read.ubus['luci.oxidns']; const write=acl.write.ubus['luci.oxidns']; if (read.includes('config_validate') || !write.includes('config_validate')) process.exit(1);"
JSON_ESCAPED="$(printf '%s\n' 'listen: "127.0.0.1:9199"' | awk 'BEGIN { ORS = "" } { if (NR > 1) printf "\\n"; for (i = 1; i <= length($0); i++) { c = substr($0, i, 1); if (c == "\\") printf "\\\\"; else if (c == "\"") printf "\\\""; else if (c == "\t") printf "\\t"; else if (c == "\r") printf "\\r"; else printf "%s", c; } }')"
test "$JSON_ESCAPED" = 'listen: \"127.0.0.1:9199\"'
PROGRESS_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/oxidns-progress-test.XXXXXX")"
printf 'original' > "$PROGRESS_TEST_DIR/target"
mkdir "$PROGRESS_TEST_DIR/luci-oxidns"
ln -s "$PROGRESS_TEST_DIR/target" "$PROGRESS_TEST_DIR/luci-oxidns/core-progress.log"
TMPDIR="$PROGRESS_TEST_DIR" root/usr/libexec/rpcd/luci.oxidns call core_reinstall >/dev/null 2>&1 || true
test "$(cat "$PROGRESS_TEST_DIR/target")" = "original"
rm -rf "$PROGRESS_TEST_DIR"
UNSAFE_UPLOAD="$(mktemp "/tmp/oxidns-core-upload-unsafe.XXXXXX")"
UNSAFE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/oxidns-core-upload-unsafe-dir.XXXXXX")"
rm -f "$UNSAFE_UPLOAD"
ln -s /etc/passwd "$UNSAFE_DIR/oxidns"
tar -czf "$UNSAFE_UPLOAD" -C "$UNSAFE_DIR" oxidns 2>/dev/null
assert_unsafe_upload_rejected "$UNSAFE_UPLOAD"
rm -f "$UNSAFE_UPLOAD" "$UNSAFE_DIR/oxidns"
mkfifo "$UNSAFE_DIR/oxidns"
tar -czf "$UNSAFE_UPLOAD" -C "$UNSAFE_DIR" oxidns 2>/dev/null
assert_unsafe_upload_rejected "$UNSAFE_UPLOAD"
rm -f "$UNSAFE_UPLOAD"
rm -rf "$UNSAFE_DIR"
printf '%s' '{"content":""}' | root/usr/libexec/rpcd/luci.oxidns call config_validate | json_ok "v.ok === false && v.code === 'missing_content'"
root/usr/libexec/rpcd/luci.oxidns call config_read | json_ok "v.ok === false && v.code === 'config_not_found'"
root/usr/libexec/rpcd/luci.oxidns call settings_read | json_ok "v.ok === true && v.log_syslog === true && v.core_repository === 'svenshi/oxidns' && v.core_bundle === 'full' && v.download_proxy === '' && v.github_token === '' && v.download_proxy_set === false && v.github_token_set === false && !('api_base_url' in v)"
FAKE_UCI_DIR="$(mktemp -d "${TMPDIR:-/tmp}/oxidns-fake-uci.XXXXXX")"
FAKE_UCI_STATE="$FAKE_UCI_DIR/state"
cat > "$FAKE_UCI_DIR/uci" <<'EOF'
#!/bin/sh
set -eu
[ "${1:-}" = "-q" ] && shift
CMD="${1:-}"
shift || true
STATE="${FAKE_UCI_STATE:?}"
case "$CMD" in
get)
KEY="${1:-}"
[ -f "$STATE" ] || exit 1
grep -F "$KEY=" "$STATE" | tail -n 1 | sed 's/^[^=]*=//' || exit 1
;;
set)
ASSIGNMENT="${1:-}"
KEY="${ASSIGNMENT%%=*}"
VALUE="${ASSIGNMENT#*=}"
TMP_STATE="$STATE.tmp"
if [ -f "$STATE" ]; then
grep -F -v "$KEY=" "$STATE" > "$TMP_STATE" || true
else
: > "$TMP_STATE"
fi
printf '%s=%s\n' "$KEY" "$VALUE" >> "$TMP_STATE"
mv "$TMP_STATE" "$STATE"
;;
delete)
KEY="${1:-}"
TMP_STATE="$STATE.tmp"
if [ -f "$STATE" ]; then
grep -F -v "$KEY=" "$STATE" > "$TMP_STATE" || true
mv "$TMP_STATE" "$STATE"
fi
;;
commit)
exit 0
;;
*)
exit 1
;;
esac
EOF
chmod 755 "$FAKE_UCI_DIR/uci"
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_read |
json_ok "v.ok === true && v.log_syslog === true"
for LOG_SYSLOG_TEST in 0 1 0; do
printf '{"log_syslog":"%s"}' "$LOG_SYSLOG_TEST" |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === true && v.log_syslog === ($LOG_SYSLOG_TEST === 1)"
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_read |
json_ok "v.ok === true && v.log_syslog === ($LOG_SYSLOG_TEST === 1)"
done
printf '%s' '{}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === true && v.log_syslog === false"
printf '%s' '{"log_syslog":"invalid"}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === false && v.code === 'invalid_log_syslog'"
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_read |
json_ok "v.ok === true && v.log_syslog === false"
printf '%s' '{"core_repository":"svenshi/oxidns","core_bundle":"full","config_path":"etc/oxidns/config.yaml","working_dir":"/var/lib/oxidns","download_proxy":"","github_token":""}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === false && v.code === 'invalid_config_path'"
printf '%s' '{"core_repository":"svenshi/oxidns","core_bundle":"full","config_path":"/etc/oxidns/config.yaml","working_dir":"/var","download_proxy":"","github_token":""}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === false && v.code === 'invalid_working_dir'"
printf '%s' '{"core_repository":"svenshi/oxidns","core_bundle":"full","config_path":"/etc/oxidns/config.yaml","working_dir":"/var/lib/oxidns","download_proxy":"","github_token":""}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === true && v.core_repository === 'svenshi/oxidns' && v.config_path === '/etc/oxidns/config.yaml' && v.working_dir === '/var/lib/oxidns' && v.download_proxy === '' && v.download_proxy_set === false"
printf '%s' '{"core_repository":"svenshi/oxidns","core_bundle":"full","config_path":"/etc/oxidns/config.yaml","working_dir":"/var/lib/oxidns","download_proxy":"http://user:pass@example.invalid:8080","github_token":"github-secret-token"}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === true && v.download_proxy === 'http://user:pass@example.invalid:8080' && v.github_token === '' && v.download_proxy_set === true && v.github_token_set === true && JSON.stringify(v).indexOf('github-secret-token') === -1"
printf '%s' '{"core_repository":"svenshi/oxidns","core_bundle":"full","config_path":"/etc/oxidns/config.yaml","working_dir":"/var/lib/oxidns","download_proxy":"","github_token":""}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === true && v.download_proxy_set === true && v.github_token === '' && v.github_token_set === true"
printf '%s' '{"core_repository":"svenshi/oxidns","core_bundle":"full","config_path":"/etc/oxidns/config.yaml","working_dir":"/var/lib/oxidns","download_proxy":"","github_token":"","clear_download_proxy":"true","clear_github_token":"true"}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" root/usr/libexec/rpcd/luci.oxidns call settings_save |
json_ok "v.ok === true && v.download_proxy === '' && v.download_proxy_set === false && v.github_token_set === false"
cat > "$FAKE_UCI_STATE" <<'EOF'
oxidns.main.core_repository=svenshi/oxidns
oxidns.main.core_bundle=full
oxidns.main.config_path=/etc/oxidns/config.yaml
oxidns.main.working_dir=/var
oxidns.main.download_proxy=
oxidns.main.github_token=
EOF
REMOVE_PREFLIGHT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/oxidns-remove-preflight.XXXXXX")"
printf '%s' '{"remove_workdir":"true"}' |
PATH="$FAKE_UCI_DIR:$PATH" FAKE_UCI_STATE="$FAKE_UCI_STATE" TMPDIR="$REMOVE_PREFLIGHT_DIR" root/usr/libexec/rpcd/luci.oxidns call core_remove |
json_ok "v.ok === false && v.code === 'unsafe_working_dir'"
test -f "$REMOVE_PREFLIGHT_DIR/luci-oxidns/core-progress.log"
! grep -q 'rm -f /usr/bin/oxidns' "$REMOVE_PREFLIGHT_DIR/luci-oxidns/core-progress.log"
! grep -q 'rm -rf /usr/share/oxidns/webui' "$REMOVE_PREFLIGHT_DIR/luci-oxidns/core-progress.log"
rm -rf "$REMOVE_PREFLIGHT_DIR"
rm -rf "$FAKE_UCI_DIR"
printf '%s' '{"limit":"20"}' | root/usr/libexec/rpcd/luci.oxidns call logs_recent | json_ok "v.ok === true && v.source === 'logread' && Array.isArray(v.lines) && !('entries' in v)"
DIST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/luci-app-oxidns-dist.XXXXXX")"
scripts/build-luci-package.sh 0.1.0 "$DIST_DIR" >/dev/null
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.ipk" control.tar.gz
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.ipk" data.tar.gz
tar_nested_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.ipk" control.tar.gz postinst
tar_nested_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.ipk" control.tar.gz postrm
tar_nested_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.ipk" data.tar.gz etc/init.d/oxidns
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.ipk" control.tar.gz
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.ipk" data.tar.gz
tar_nested_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.ipk" control.tar.gz postinst
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" .PKGINFO
tar_member_contains "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" .PKGINFO '^arch = noarch$'
tar_member_contains "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" .PKGINFO '^datahash = [0-9a-f][0-9a-f]*$'
apk_data_has_checksum "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk"
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" etc
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" etc/config
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" usr/share/luci/menu.d
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" www/luci-static/resources/view/oxidns
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" usr/libexec/rpcd/luci.oxidns
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" etc/init.d/oxidns
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" .post-install
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" .post-upgrade
tar_has_member "$DIST_DIR/luci-app-oxidns_0.1.0-r1_all.apk" .post-deinstall
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" .PKGINFO
tar_member_contains "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" .PKGINFO '^arch = noarch$'
tar_member_contains "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" .PKGINFO '^datahash = [0-9a-f][0-9a-f]*$'
apk_data_has_checksum "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk"
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" usr/lib/lua/luci/i18n
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" usr/lib/lua/luci/i18n/oxidns.zh-cn.lmo
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" .post-install
tar_has_member "$DIST_DIR/luci-i18n-oxidns-zh-cn_0.1.0-r1_all.apk" .post-upgrade
rm -rf "$DIST_DIR"
-252
View File
@@ -1,252 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs';
function usage() {
console.error('Usage: po2lmo.mjs input.po output.lmo');
process.exit(1);
}
function parsePoString(value) {
try {
return JSON.parse(value);
} catch (err) {
throw new Error(`invalid PO string ${value}: ${err.message}`);
}
}
function newEntry() {
return {
msgctxt: null,
msgid: null,
msgidPlural: null,
msgstr: []
};
}
function parsePo(content) {
const entries = [];
let entry = newEntry();
let current = null;
let currentPlural = 0;
function hasContent(item) {
return item.msgctxt !== null || item.msgid !== null || item.msgidPlural !== null || item.msgstr.length > 0;
}
function flush() {
if (hasContent(entry))
entries.push(entry);
entry = newEntry();
current = null;
currentPlural = 0;
}
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (line === '') {
flush();
continue;
}
if (line.startsWith('#'))
continue;
let match = line.match(/^msgctxt\s+(".*")$/);
if (match) {
entry.msgctxt = parsePoString(match[1]);
current = 'msgctxt';
continue;
}
match = line.match(/^msgid\s+(".*")$/);
if (match) {
if (entry.msgid !== null || entry.msgstr.length > 0)
flush();
entry.msgid = parsePoString(match[1]);
current = 'msgid';
continue;
}
match = line.match(/^msgid_plural\s+(".*")$/);
if (match) {
entry.msgidPlural = parsePoString(match[1]);
current = 'msgidPlural';
continue;
}
match = line.match(/^msgstr(?:\[(\d+)])?\s+(".*")$/);
if (match) {
currentPlural = Number(match[1] || '0');
entry.msgstr[currentPlural] = parsePoString(match[2]);
current = 'msgstr';
continue;
}
match = line.match(/^(".*")$/);
if (match) {
const fragment = parsePoString(match[1]);
if (current === 'msgctxt')
entry.msgctxt += fragment;
else if (current === 'msgid')
entry.msgid += fragment;
else if (current === 'msgidPlural')
entry.msgidPlural += fragment;
else if (current === 'msgstr')
entry.msgstr[currentPlural] = (entry.msgstr[currentPlural] || '') + fragment;
else
throw new Error(`unexpected continued PO string: ${rawLine}`);
continue;
}
throw new Error(`unsupported PO line: ${rawLine}`);
}
flush();
return entries;
}
function add32(value, delta) {
return (value + delta) >>> 0;
}
function get16(buffer, offset) {
return (((buffer[offset + 1] || 0) << 8) + (buffer[offset] || 0)) >>> 0;
}
function signedByte(value) {
return value > 127 ? value - 256 : value;
}
function sfhHash(value) {
const buffer = Buffer.from(value, 'utf8');
let len = buffer.length;
let hash = len >>> 0;
let offset = 0;
const rem = len & 3;
if (len <= 0)
return 0;
len >>>= 2;
for (; len > 0; len--) {
hash = add32(hash, get16(buffer, offset));
const tmp = (((get16(buffer, offset + 2) << 11) >>> 0) ^ hash) >>> 0;
hash = (((hash << 16) >>> 0) ^ tmp) >>> 0;
offset += 4;
hash = add32(hash, hash >>> 11);
}
switch (rem) {
case 3:
hash = add32(hash, get16(buffer, offset));
hash = (hash ^ ((hash << 16) >>> 0)) >>> 0;
hash = (hash ^ ((signedByte(buffer[offset + 2]) << 18) >>> 0)) >>> 0;
hash = add32(hash, hash >>> 11);
break;
case 2:
hash = add32(hash, get16(buffer, offset));
hash = (hash ^ ((hash << 11) >>> 0)) >>> 0;
hash = add32(hash, hash >>> 17);
break;
case 1:
hash = add32(hash, signedByte(buffer[offset]));
hash = (hash ^ ((hash << 10) >>> 0)) >>> 0;
hash = add32(hash, hash >>> 1);
break;
}
hash = (hash ^ ((hash << 3) >>> 0)) >>> 0;
hash = add32(hash, hash >>> 5);
hash = (hash ^ ((hash << 4) >>> 0)) >>> 0;
hash = add32(hash, hash >>> 17);
hash = (hash ^ ((hash << 25) >>> 0)) >>> 0;
hash = add32(hash, hash >>> 6);
return hash >>> 0;
}
function pluralForms(header) {
for (const line of header.split('\n')) {
if (line.toLowerCase().startsWith('plural-forms: '))
return line.slice(14).trim();
}
return '';
}
function writeLmo(entries, outputPath) {
const chunks = [];
const index = [];
let offset = 0;
function addEntry(key, value, valueCount, keyId) {
if (!value)
return;
const keyHash = keyId ?? sfhHash(key);
const valueHash = sfhHash(value);
if (keyHash === valueHash)
return;
const data = Buffer.from(value, 'utf8');
index.push({
keyId: keyHash >>> 0,
valueId: valueCount >>> 0,
offset,
length: data.length
});
chunks.push(data);
const padding = (4 - (data.length % 4)) % 4;
if (padding)
chunks.push(Buffer.alloc(padding));
offset += data.length + padding;
}
for (const entry of entries) {
if (entry.msgid === '')
addEntry('', pluralForms(entry.msgstr[0] || ''), 0, 0);
else if (entry.msgid !== null && entry.msgidPlural !== null) {
const valueCount = entry.msgstr.length;
for (let i = 0; i < entry.msgstr.length; i++) {
const key = entry.msgctxt
? `${entry.msgctxt}\x01${entry.msgid}\x02${i}`
: `${entry.msgid}\x02${i}`;
addEntry(key, entry.msgstr[i], valueCount);
}
}
else if (entry.msgid !== null && entry.msgstr[0])
addEntry(entry.msgctxt ? `${entry.msgctxt}\x01${entry.msgid}` : entry.msgid, entry.msgstr[0], 1);
}
if (index.length === 0) {
if (fs.existsSync(outputPath))
fs.unlinkSync(outputPath);
return;
}
index.sort((a, b) => a.keyId - b.keyId);
const indexBuffer = Buffer.alloc(index.length * 16 + 4);
for (let i = 0; i < index.length; i++) {
const base = i * 16;
indexBuffer.writeUInt32BE(index[i].keyId, base);
indexBuffer.writeUInt32BE(index[i].valueId, base + 4);
indexBuffer.writeUInt32BE(index[i].offset, base + 8);
indexBuffer.writeUInt32BE(index[i].length, base + 12);
}
indexBuffer.writeUInt32BE(offset, index.length * 16);
fs.writeFileSync(outputPath, Buffer.concat([...chunks, indexBuffer]));
}
const [, , inputPath, outputPath] = process.argv;
if (!inputPath || !outputPath)
usage();
writeLmo(parsePo(fs.readFileSync(inputPath, 'utf8')), outputPath);
-108
View File
@@ -1,108 +0,0 @@
#!/bin/sh
set -eu
VERSION="${1:-${VERSION:-0.1.0}}"
OUT_DIR="${2:-${OUT_DIR:-dist}}"
PKG_VERSION="$(printf '%s' "$VERSION" | sed 's/^v//')"
PKG_BASE="luci-app-oxidns_${PKG_VERSION}-r1_all"
I18N_BASE="luci-i18n-oxidns-zh-cn_${PKG_VERSION}-r1_all"
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'required command not found: %s\n' "$1" >&2
exit 1
}
}
tar_has_member() {
tar -tzf "$1" | awk -v member="$2" '
{
path = $0;
sub(/^\.\//, "", path);
sub(/\/$/, "", path);
if (path == member)
found = 1;
}
END { exit found ? 0 : 1 }
'
}
tar_member_contains() {
tar -xOzf "$1" "$2" 2>/dev/null | grep -q "$3"
}
apk_data_has_checksum() {
gzip -dc "$1" | grep -q 'APK-TOOLS.checksum.SHA1='
}
tar_nested_has_member() {
outer="$1"
inner="$2"
member="$3"
nested="$(mktemp "${TMPDIR:-/tmp}/luci-app-oxidns-nested.XXXXXX")"
if ! tar -xOf "$outer" "$inner" > "$nested" 2>/dev/null &&
! tar -xOf "$outer" "./$inner" > "$nested" 2>/dev/null; then
rm -f "$nested"
return 1
fi
if tar -tzf "$nested" | awk -v member="$member" '
{
path = $0;
sub(/^\.\//, "", path);
if (path == member)
found = 1;
}
END { exit found ? 0 : 1 }
'; then
rm -f "$nested"
return 0
fi
rm -f "$nested"
return 1
}
need_cmd awk
need_cmd gzip
need_cmd grep
need_cmd sha256sum
need_cmd tar
scripts/check.sh
scripts/integration-check.sh
scripts/build-luci-package.sh "$VERSION" "$OUT_DIR"
tar_has_member "$OUT_DIR/${PKG_BASE}.ipk" control.tar.gz
tar_has_member "$OUT_DIR/${PKG_BASE}.ipk" data.tar.gz
tar_nested_has_member "$OUT_DIR/${PKG_BASE}.ipk" control.tar.gz postinst
tar_nested_has_member "$OUT_DIR/${PKG_BASE}.ipk" control.tar.gz postrm
tar_nested_has_member "$OUT_DIR/${PKG_BASE}.ipk" data.tar.gz etc/init.d/oxidns
tar_has_member "$OUT_DIR/${I18N_BASE}.ipk" control.tar.gz
tar_has_member "$OUT_DIR/${I18N_BASE}.ipk" data.tar.gz
tar_nested_has_member "$OUT_DIR/${I18N_BASE}.ipk" control.tar.gz postinst
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" .PKGINFO
tar_member_contains "$OUT_DIR/${PKG_BASE}.apk" .PKGINFO '^arch = noarch$'
tar_member_contains "$OUT_DIR/${PKG_BASE}.apk" .PKGINFO '^datahash = [0-9a-f][0-9a-f]*$'
apk_data_has_checksum "$OUT_DIR/${PKG_BASE}.apk"
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" etc
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" etc/config
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" usr/share/luci/menu.d
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" www/luci-static/resources/view/oxidns
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" usr/libexec/rpcd/luci.oxidns
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" etc/init.d/oxidns
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" .post-install
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" .post-upgrade
tar_has_member "$OUT_DIR/${PKG_BASE}.apk" .post-deinstall
tar_has_member "$OUT_DIR/${I18N_BASE}.apk" .PKGINFO
tar_member_contains "$OUT_DIR/${I18N_BASE}.apk" .PKGINFO '^arch = noarch$'
tar_member_contains "$OUT_DIR/${I18N_BASE}.apk" .PKGINFO '^datahash = [0-9a-f][0-9a-f]*$'
apk_data_has_checksum "$OUT_DIR/${I18N_BASE}.apk"
tar_has_member "$OUT_DIR/${I18N_BASE}.apk" usr/lib/lua/luci/i18n
tar_has_member "$OUT_DIR/${I18N_BASE}.apk" usr/lib/lua/luci/i18n/oxidns.zh-cn.lmo
tar_has_member "$OUT_DIR/${I18N_BASE}.apk" .post-install
tar_has_member "$OUT_DIR/${I18N_BASE}.apk" .post-upgrade
(cd "$OUT_DIR" && sha256sum -c sha256sums.txt)
printf 'Release check passed for %s in %s\n' "$VERSION" "$OUT_DIR"
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs';
const [input, output] = process.argv.slice(2);
if (!input || !output) {
console.error('usage: strip-tar-eof.mjs <input.tar> <output.tar>');
process.exit(1);
}
const blockSize = 512;
const data = fs.readFileSync(input);
let end = data.length;
function isZeroBlock(offset) {
for (let i = offset; i < offset + blockSize; i++) {
if (data[i] !== 0)
return false;
}
return true;
}
while (end >= blockSize && isZeroBlock(end - blockSize))
end -= blockSize;
fs.writeFileSync(output, data.subarray(0, end));
@@ -1,177 +0,0 @@
#!/usr/bin/env node
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
const [rootDir, outFile] = process.argv.slice(2);
if (!rootDir || !outFile) {
console.error('usage: write-apk-data-tar.mjs <root-dir> <out.tar>');
process.exit(1);
}
const blockSize = 512;
const root = path.resolve(rootDir);
const chunks = [];
function padBuffer(buffer) {
const remainder = buffer.length % blockSize;
if (remainder !== 0)
chunks.push(Buffer.alloc(blockSize - remainder));
}
function paxLine(key, value) {
const body = `${key}=${value}\n`;
let length = Buffer.byteLength(body) + 3;
for (;;) {
const candidate = `${length} ${body}`;
const actual = Buffer.byteLength(candidate);
if (actual === length)
return candidate;
length = actual;
}
}
function writeString(buffer, offset, length, value) {
buffer.fill(0, offset, offset + length);
Buffer.from(value).copy(buffer, offset, 0, length);
}
function writeOctal(buffer, offset, length, value) {
const text = value.toString(8).padStart(length - 1, '0');
writeString(buffer, offset, length, text);
}
function splitName(name) {
const bytes = Buffer.byteLength(name);
if (bytes <= 100)
return { name, prefix: '' };
const parts = name.split('/');
for (let i = 1; i < parts.length; i++) {
const prefix = parts.slice(0, i).join('/');
const suffix = parts.slice(i).join('/');
if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(suffix) <= 100)
return { name: suffix, prefix };
}
throw new Error(`tar path too long: ${name}`);
}
function tarHeader({ name, mode, size, type, linkname = '', uname = 'root', gname = 'root', mtime = 0 }) {
const header = Buffer.alloc(blockSize, 0);
const split = splitName(name);
writeString(header, 0, 100, split.name);
writeOctal(header, 100, 8, mode & 0o7777);
writeOctal(header, 108, 8, 0);
writeOctal(header, 116, 8, 0);
writeOctal(header, 124, 12, size);
writeOctal(header, 136, 12, mtime);
header.fill(0x20, 148, 156);
writeString(header, 156, 1, type);
writeString(header, 157, 100, linkname);
writeString(header, 257, 6, 'ustar');
writeString(header, 263, 2, '00');
writeString(header, 265, 32, uname);
writeString(header, 297, 32, gname);
writeOctal(header, 329, 8, 0);
writeOctal(header, 337, 8, 0);
writeString(header, 345, 155, split.prefix);
let checksum = 0;
for (const byte of header)
checksum += byte;
const checksumText = checksum.toString(8).padStart(6, '0');
writeString(header, 148, 8, `${checksumText}\0 `);
return header;
}
function writeEntry(entry) {
const paxHeaders = {
ctime: '0',
atime: '0',
...entry.pax,
};
const paxContent = Object.entries(paxHeaders).map(([key, value]) => paxLine(key, value)).join('');
const paxData = Buffer.from(paxContent);
const paxName = `PaxHeaders/${path.basename(entry.name) || 'root'}`;
chunks.push(tarHeader({
name: paxName,
mode: 0o644,
size: paxData.length,
type: 'x',
uname: '',
gname: '',
}));
chunks.push(paxData);
padBuffer(paxData);
chunks.push(tarHeader(entry));
if (entry.data) {
chunks.push(entry.data);
padBuffer(entry.data);
}
}
function collect(dir, base = '') {
const names = fs.readdirSync(dir).sort();
const entries = [];
for (const name of names) {
const absolute = path.join(dir, name);
const relative = base ? `${base}/${name}` : name;
const stat = fs.lstatSync(absolute);
entries.push({ absolute, relative, stat });
if (stat.isDirectory())
entries.push(...collect(absolute, relative));
}
return entries;
}
for (const item of collect(root)) {
if (item.stat.isDirectory()) {
writeEntry({
name: item.relative,
mode: item.stat.mode,
size: 0,
type: '5',
pax: {},
});
} else if (item.stat.isFile()) {
const data = fs.readFileSync(item.absolute);
const sha1 = crypto.createHash('sha1').update(data).digest('hex');
writeEntry({
name: item.relative,
mode: item.stat.mode,
size: data.length,
type: '0',
data,
pax: {
'APK-TOOLS.checksum.SHA1': sha1,
},
});
} else if (item.stat.isSymbolicLink()) {
const target = fs.readlinkSync(item.absolute);
const sha1 = crypto.createHash('sha1').update(target).digest('hex');
writeEntry({
name: item.relative,
mode: item.stat.mode,
size: 0,
type: '2',
linkname: target,
pax: {
'APK-TOOLS.checksum.SHA1': sha1,
},
});
}
}
chunks.push(Buffer.alloc(blockSize * 2));
fs.writeFileSync(outFile, Buffer.concat(chunks));
@@ -1,44 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
function usage() {
console.error('usage: write-ar-archive.mjs <out.ar> <member-name=path>...');
process.exit(1);
}
const [, , outPath, ...memberSpecs] = process.argv;
if (!outPath || memberSpecs.length === 0)
usage();
const chunks = [ Buffer.from('!<arch>\n', 'ascii') ];
for (const spec of memberSpecs) {
const sep = spec.indexOf('=');
const memberName = sep >= 0 ? spec.slice(0, sep) : path.basename(spec);
const sourcePath = sep >= 0 ? spec.slice(sep + 1) : spec;
if (!memberName || memberName.length > 16 || /[\/\s]/.test(memberName)) {
console.error(`unsupported ar member name: ${memberName}`);
process.exit(1);
}
const data = fs.readFileSync(sourcePath);
const header = [
memberName.padEnd(16, ' '),
'0'.padEnd(12, ' '),
'0'.padEnd(6, ' '),
'0'.padEnd(6, ' '),
'100644'.padEnd(8, ' '),
String(data.length).padEnd(10, ' '),
'`\n'
].join('');
chunks.push(Buffer.from(header, 'ascii'), data);
if (data.length % 2 === 1)
chunks.push(Buffer.from('\n', 'ascii'));
}
fs.writeFileSync(outPath, Buffer.concat(chunks));