#!/usr/bin/env bash # Reproducible corresponding-source recipe for the KZ Returns HEIC decoder. # # Usage: # bash kz-heic-decoder-rebuild-1.5.2-heif-1.23.1-de265-1.1.1.txt BUILD_DIR # # BUILD_DIR must not exist. The verified bundle is written to BUILD_DIR/out/. # Required host tools: bash, curl, git, Python 3.10+, tar, and a C/C++ build tool. # Network access is used only to obtain the immutable sources and toolchain below. set -euo pipefail readonly HEIC_TO_COMMIT='f37af866f9aa6212ddc84b67a279c9f2386aba4f' readonly LIBHEIF_COMMIT='2c4bbb54c2738d4a5efbbe3e5fa1d5d76bb88eb0' readonly LIBDE265_COMMIT='4dd701fffac01632ffd5cabc5ef10deb56accba1' readonly EMSDK_COMMIT='9fcdf593953edfcddb297572d7f2177d336b0479' readonly EMSCRIPTEN_VERSION='3.1.61' readonly EMSCRIPTEN_COMMIT='67fa4c16496b157a7fc3377afd69ee0445e8a6e3' readonly CMAKE_VERSION='4.0.0' readonly NODE_VERSION='22.16.0' readonly LIBDE265_ARCHIVE_SHA256='fd48a927e94ed74fc7ce8829d222b9d8599fcbfe8b6448ba66705babc56ab219' readonly LIBDE265_LIBRARY_SHA256='bd120c5b8c1bd4c484ef17289e98d2becf402f7fdb74eb2d05a713e08ae9fde5' readonly LIBHEIF_GENERATED_SHA256='add9beb986e3abb9397f058f587579374a4cee1714cd508ee97e444cf2beff34' readonly LIBHEIF_ESM_SHA256='3ddba52d56c4bd9ad6a576bc3ec0a697099ca9a909d43b87dc9db047c82f48bb' readonly BUNDLE_SHA256='0245e7ecc3d4d9d9e504192083645a8ca5943fb0239093bfe9b93cc88aa13485' readonly OUTPUT_NAME='kz-heic-to-1.5.2-heif-1.23.1-de265-1.1.1.csp.min.js' die() { printf 'error: %s\n' "$*" >&2 exit 1 } sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' else shasum -a 256 "$1" | awk '{print $1}' fi } verify_sha256() { local file="$1" local expected="$2" local actual actual="$(sha256 "$file")" [ "$actual" = "$expected" ] || die "SHA-256 mismatch for $file: $actual" } fetch_commit() { local url="$1" local commit="$2" local destination="$3" git init -q "$destination" git -C "$destination" remote add origin "$url" git -C "$destination" fetch -q --depth 1 origin "$commit" git -C "$destination" checkout -q --detach FETCH_HEAD [ "$(git -C "$destination" rev-parse HEAD)" = "$commit" ] || die "unexpected commit in $destination" } select_python() { local candidate if [ -n "${PYTHON:-}" ]; then "$PYTHON" -c 'import sys; raise SystemExit(sys.version_info < (3, 10))' 2>/dev/null \ && printf '%s\n' "$PYTHON" \ && return 0 fi for candidate in python3.13 python3.12 python3.11 python3.10 python3; do if command -v "$candidate" >/dev/null 2>&1 \ && "$candidate" -c 'import sys; raise SystemExit(sys.version_info < (3, 10))' 2>/dev/null; then command -v "$candidate" return 0 fi done return 1 } [ "$#" -eq 1 ] || die 'pass one new BUILD_DIR path' readonly WORK_DIR="$1" [ ! -e "$WORK_DIR" ] || die "BUILD_DIR already exists: $WORK_DIR" mkdir -p "$WORK_DIR" readonly WORK_ABS="$(cd "$WORK_DIR" && pwd)" readonly EMSDK_DIR="$WORK_ABS/emsdk" readonly HEIC_TO_DIR="$WORK_ABS/heic-to" readonly LIBHEIF_DIR="$WORK_ABS/libheif" readonly LIBHEIF_BUILD="$WORK_ABS/libheif-build" readonly LIBDE265_ARCHIVE="$LIBHEIF_BUILD/libde265-1.1.1.tar.gz" readonly LIBDE265_SOURCE="$LIBHEIF_BUILD/libde265-1.1.1" readonly LIBDE265_BUILD="$LIBHEIF_BUILD/libde265-cmake" readonly CMAKE_VENV="$WORK_ABS/cmake-venv" readonly OUTPUT_DIR="$WORK_ABS/out" HOST_PYTHON="$(select_python)" || die 'Python 3.10 or newer is required' readonly HOST_PYTHON fetch_commit 'https://github.com/emscripten-core/emsdk.git' "$EMSDK_COMMIT" "$EMSDK_DIR" fetch_commit 'https://github.com/hoppergee/heic-to.git' "$HEIC_TO_COMMIT" "$HEIC_TO_DIR" fetch_commit 'https://github.com/strukturag/libheif.git' "$LIBHEIF_COMMIT" "$LIBHEIF_DIR" mkdir -p "$LIBHEIF_BUILD" curl --fail --location --proto '=https' --tlsv1.2 \ 'https://github.com/strukturag/libde265/releases/download/v1.1.1/libde265-1.1.1.tar.gz' \ --output "$LIBDE265_ARCHIVE" verify_sha256 "$LIBDE265_ARCHIVE" "$LIBDE265_ARCHIVE_SHA256" tar -xzf "$LIBDE265_ARCHIVE" -C "$LIBHEIF_BUILD" "$HOST_PYTHON" -m venv "$CMAKE_VENV" "$CMAKE_VENV/bin/python" -m pip install --disable-pip-version-check --quiet "cmake==$CMAKE_VERSION" ( cd "$EMSDK_DIR" "$HOST_PYTHON" ./emsdk.py install "$EMSCRIPTEN_VERSION" "$HOST_PYTHON" ./emsdk.py activate "$EMSCRIPTEN_VERSION" ) # shellcheck disable=SC1091 source "$EMSDK_DIR/emsdk_env.sh" >/dev/null export PATH="$(dirname "$EMSDK_NODE"):$CMAKE_VENV/bin:$PATH" emcc --version | grep -F "$EMSCRIPTEN_VERSION ($EMSCRIPTEN_COMMIT)" >/dev/null \ || die 'unexpected Emscripten compiler' [ "$(node --version)" = "v$NODE_VERSION" ] || die 'unexpected Node.js version' [ "$(cmake --version | awk 'NR == 1 {print $3}')" = "$CMAKE_VERSION" ] || die 'unexpected CMake version' readonly JOBS="${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '4')}" emcmake cmake -S "$LIBDE265_SOURCE" -B "$LIBDE265_BUILD" \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_DECODER=OFF \ -DENABLE_ENCODER=OFF \ -DENABLE_INTERNAL_DEVELOPMENT_TOOLS=OFF \ -DENABLE_SDL=OFF \ -DENABLE_SHERLOCK265=OFF \ -DENABLE_SIMD=OFF \ -DWITH_FUZZERS=OFF cmake --build "$LIBDE265_BUILD" --parallel "$JOBS" verify_sha256 "$LIBDE265_BUILD/libde265/libde265.a" "$LIBDE265_LIBRARY_SHA256" # libheif 1.23.1's helper still expects libde265's former autotools layout. # Populate only those generated paths; no upstream C or C++ source is changed. mkdir -p "$LIBDE265_SOURCE/libde265/.libs" cp "$LIBDE265_BUILD/libde265/libde265.a" "$LIBDE265_SOURCE/libde265/.libs/libde265.a" cp "$LIBDE265_BUILD/libde265/de265-version.h" "$LIBDE265_SOURCE/libde265/de265-version.h" ( cd "$LIBHEIF_BUILD" CORES="$JOBS" \ LIBDE265_VERSION=1.1.1 \ USE_UNSAFE_EVAL=0 \ USE_WASM=0 \ USE_TYPESCRIPT=0 \ "$LIBHEIF_DIR/build-emscripten.sh" "$LIBHEIF_DIR" ) verify_sha256 "$LIBHEIF_BUILD/libheif.js" "$LIBHEIF_GENERATED_SHA256" # Convert the generated UMD footer to the ESM factory expected by heic-to. # The transformation is deliberately exact and aborts if Emscripten changes it. node - "$LIBHEIF_BUILD/libheif.js" "$HEIC_TO_DIR/src/lib/libheif-without-unsafe-eval.js" <<'NODE' const fs = require('fs'); const [input, output] = process.argv.slice(2); let source = fs.readFileSync(input, 'utf8'); const banner = '// KZ security rebuild: libheif 1.23.1; libde265 1.1.1; Emscripten 3.1.61; USE_WASM=0; USE_UNSAFE_EVAL=0'; const factoryBefore = '\nvar libheif = (() => {'; const factoryAfter = `\n${banner}\nvar buildLibheif = (() => {`; const umdBefore = `if (typeof exports === 'object' && typeof module === 'object')\n module.exports = libheif;\nelse if (typeof define === 'function' && define['amd'])\n define([], () => libheif);\n`; if (!source.includes(factoryBefore) || !source.endsWith(umdBefore)) { throw new Error('unexpected generated libheif wrapper'); } source = source.replace(factoryBefore, factoryAfter) .slice(0, -umdBefore.length) + '\nexport default buildLibheif\n'; fs.writeFileSync(output, source); NODE verify_sha256 "$HEIC_TO_DIR/src/lib/libheif-without-unsafe-eval.js" "$LIBHEIF_ESM_SHA256" ( cd "$HEIC_TO_DIR" npm ci npm run build ) verify_sha256 "$HEIC_TO_DIR/dist/csp/heic-to.js" "$BUNDLE_SHA256" grep -Eq 'eval[[:space:]]*\(|new[[:space:]]+Function[[:space:]]*\(' "$HEIC_TO_DIR/dist/csp/heic-to.js" \ && die 'dynamic code execution found in output' grep -Eq 'WebAssembly\.(compile|instantiate|instantiateStreaming)' "$HEIC_TO_DIR/dist/csp/heic-to.js" \ && die 'WebAssembly runtime call found in output' mkdir -p "$OUTPUT_DIR" cp "$HEIC_TO_DIR/dist/csp/heic-to.js" "$OUTPUT_DIR/$OUTPUT_NAME" printf 'verified %s %s\n' "$BUNDLE_SHA256" "$OUTPUT_DIR/$OUTPUT_NAME"