mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-01 08:03:56 +08:00
update 2026-10-07 11:52:44
This commit is contained in:
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
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
@@ -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));
|
||||
Reference in new issue
Block a user