diff --git a/.cargo/config.toml b/.cargo/config.toml index 36120f00..d1ef850b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,6 +2,10 @@ # one disposable build tree. Cargo resolves this relative path to /target. [build] target-dir = "target" +# Compiling Slint's generated library and test harness together can exhaust +# memory on a 16 GiB development host, including editor background checks. +# Larger builders can opt into more parallelism with Cargo's -j option. +jobs = 1 # Slint's desktop guide recommends matching Linux's larger main-thread stack on # Windows. This prevents stack overflows in debug builds with deep UI trees. @@ -10,3 +14,11 @@ rustflags = ["-C", "link-arg=/STACK:8000000"] [target.aarch64-pc-windows-msvc] rustflags = ["-C", "link-arg=/STACK:8000000"] + +# NDK r27 and newer devices must agree on 16 KiB ELF load alignment. This also +# remains compatible with 4 KiB Android devices and matches bundled PDFium. +[target.aarch64-linux-android] +rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] + +[target.x86_64-linux-android] +rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] diff --git a/.github/workflows/native-platforms.yml b/.github/workflows/native-platforms.yml index 19738ae9..8923442f 100644 --- a/.github/workflows/native-platforms.yml +++ b/.github/workflows/native-platforms.yml @@ -210,6 +210,8 @@ jobs: New-Item -ItemType Directory -Force target/package/windows | Out-Null New-Item -ItemType Directory -Force "target/package/windows/Licenses/Google Sans Flex" | Out-Null New-Item -ItemType Directory -Force "target/package/windows/Licenses/Noto Emoji" | Out-Null + python scripts/stage-pdfium.py win-x64 target/package/windows + if ($LASTEXITCODE -ne 0) { throw "PDFium staging failed" } Copy-Item target/release/flectar-mail.exe target/package/windows/ Copy-Item README.md target/package/windows/ Copy-Item LICENSE target/package/windows/ @@ -219,6 +221,8 @@ jobs: Copy-Item resources/fonts/google-sans-flex/README.md "target/package/windows/Licenses/Google Sans Flex/" Copy-Item resources/fonts/noto-emoji/OFL.txt "target/package/windows/Licenses/Noto Emoji/" Copy-Item resources/fonts/noto-emoji/README.md "target/package/windows/Licenses/Noto Emoji/" + python scripts/test-pdf-preview.py target/package/windows/flectar-mail.exe + if ($LASTEXITCODE -ne 0) { throw "Packaged PDF preview test failed" } Compress-Archive -Path target/package/windows/* -DestinationPath target/flectar-mail-windows-x64.zip -Force - name: Install pinned Inno Setup compiler diff --git a/.gitignore b/.gitignore index be2ebca4..378fd4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.AppImage /platform/android/gradle/.gradle/ /platform/android/gradle/app/build/ +/platform/android/gradle/build/ /platform/ios/*.xcodeproj/ # Local OAuth application registrations (see .env.example) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1724bb25..816823cb 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -36,6 +36,23 @@ notices and license texts; the Apache-2.0 license is also included in | percent-encoding | 2.3.2 | [servo/rust-url](https://github.com/servo/rust-url/) | Fragment and mailto decoding | | base64 | 0.22.1 | [marshallpierce/rust-base64](https://github.com/marshallpierce/rust-base64) | Export CSP hash encoding | +## PDF preview + +Desktop, Android, and iOS packages include a non-V8 PDFium runtime from +[bblanchon/pdfium-binaries](https://github.com/bblanchon/pdfium-binaries), built +from [PDFium](https://pdfium.googlesource.com/pdfium/). The selected release and +per-platform SHA-256 digests are pinned in `scripts/stage-pdfium.py`. Every +package retains the archive's `LICENSE`, complete `licenses/` directory, and +build provenance under `pdfium-licenses` beside the private desktop library, +Android assets at `licenses/PDFium`, and iOS bundle resources at `Licenses/PDFium`. +PDFium's third-party components retain their respective licenses. + +The Rust interface is [pdfium-render](https://github.com/ajrcarey/pdfium-render) +0.9.4, used under its Apache-2.0 option. Linux process restrictions use +[Landlock](https://github.com/landlock-lsm/rust-landlock) and +[seccompiler](https://github.com/rust-vmm/seccompiler), under their Apache-2.0 +options. Rust package versions and source checksums are recorded in `Cargo.lock`. + ## Phosphor Icons The bundled SVG icons in `ui/icons/phosphor` are sourced from diff --git a/platform/android/gradle/app/build.gradle.kts b/platform/android/gradle/app/build.gradle.kts index afdd912d..9c17540d 100644 --- a/platform/android/gradle/app/build.gradle.kts +++ b/platform/android/gradle/app/build.gradle.kts @@ -26,6 +26,10 @@ android { layout.buildDirectory.dir("generated/flectar-assets"), ) + // PDFium is loaded by its absolute installed nativeLibraryDir path. Let the + // package manager extract and protect it; no temporary executable files. + packaging.jniLibs.useLegacyPackaging = true + signingConfigs { create("releaseFromEnvironment") { val keystore = providers.environmentVariable("FLECTAR_ANDROID_KEYSTORE").orNull @@ -56,8 +60,28 @@ val generateFlectarLicenseAssets by tasks.registering(Copy::class) { } } +val generatePdfiumLicenseAssets by tasks.registering(Copy::class) { + into(layout.buildDirectory.dir("generated/flectar-assets/licenses/PDFium")) + from(repositoryAndroidTarget.dir("gradle-jni")) { + include("*/pdfium-licenses/**") + } +} +val verifyPdfiumRuntime by tasks.registering { + doLast { + fileTree(repositoryAndroidTarget.dir("gradle-jni")).matching { + include("*/libflectar_mail_android.so") + }.forEach { appLibrary -> + check(appLibrary.resolveSibling("libpdfium.so").isFile) { + "Missing bundled PDFium for ${appLibrary.parentFile.name}; run scripts/stage-pdfium.py for this ABI." + } + check(appLibrary.parentFile.resolve("pdfium-licenses/build.json").isFile) { + "Missing PDFium provenance and license notices." + } + } + } +} tasks.named("preBuild").configure { - dependsOn(generateFlectarLicenseAssets) + dependsOn(generateFlectarLicenseAssets, generatePdfiumLicenseAssets, verifyPdfiumRuntime) } dependencies { diff --git a/platform/ios/build-with-cargo.sh b/platform/ios/build-with-cargo.sh index 6f78040f..34ef2d96 100755 --- a/platform/ios/build-with-cargo.sh +++ b/platform/ios/build-with-cargo.sh @@ -15,11 +15,12 @@ export CARGO_PROFILE_RELEASE_DEBUG="${CARGO_PROFILE_RELEASE_DEBUG:-1}" export CARGO_TARGET_DIR="${DERIVED_FILE_DIR:?}/cargo" is_simulator=0 -if [[ "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then +if [[ "${PLATFORM_NAME:-}" == "iphonesimulator" || "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then is_simulator=1 fi executables=() +pdfium_libraries=() for arch in $ARCHS; do case "$arch" in arm64) @@ -47,10 +48,48 @@ for arch in $ARCHS; do --features remote-content,ios \ "${release_args[@]}" executables+=("$CARGO_TARGET_DIR/$cargo_target/$profile/$binary_name") + case "$cargo_target" in + aarch64-apple-ios) pdfium_target=ios-device-arm64 ;; + aarch64-apple-ios-sim) pdfium_target=ios-simulator-arm64 ;; + x86_64-apple-ios) pdfium_target=ios-simulator-x64 ;; + esac + pdfium_stage="$DERIVED_FILE_DIR/pdfium/$pdfium_target" + python3 "$project_dir/scripts/stage-pdfium.py" "$pdfium_target" "$pdfium_stage" \ + --cache "$project_dir/target/pdfium-downloads" + pdfium_libraries+=("$pdfium_stage/libpdfium.dylib") + pdfium_notices="$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/Licenses/PDFium/$pdfium_target" + mkdir -p "$pdfium_notices" + cp -R "$pdfium_stage/pdfium-licenses/." "$pdfium_notices/" done lipo -create -output "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" "${executables[@]}" +# iOS permits embedded dynamic frameworks, not loose third-party dylibs. +# Device and simulator slices are never mixed; Xcode chooses the SDK above. +framework="$TARGET_BUILD_DIR/${FRAMEWORKS_FOLDER_PATH:?}/PDFium.framework" +mkdir -p "$framework" +lipo -create -output "$framework/PDFium" "${pdfium_libraries[@]}" +chmod 755 "$framework/PDFium" +install_name_tool -id '@rpath/PDFium.framework/PDFium' "$framework/PDFium" +python3 "$project_dir/scripts/write-pdfium-framework-plist.py" "$framework/Info.plist" "$is_simulator" +if [[ "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then + # Simulator frameworks also need an ad-hoc signature after install_name_tool. + if [[ "$is_simulator" -eq 1 ]]; then + framework_identity="${EXPANDED_CODE_SIGN_IDENTITY:--}" + else + framework_identity="${EXPANDED_CODE_SIGN_IDENTITY:?A device build requires an Xcode signing identity}" + fi + codesign --force --sign "$framework_identity" "$framework" + codesign --verify --strict "$framework" +fi +# Fail packaging if an unexpected dependency or install name escaped staging. +framework_dependencies="$(otool -L "$framework/PDFium")" +if printf '%s\n' "$framework_dependencies" | awk '/^[[:space:]]/ {print $1}' \ + | grep -Ev '^(@rpath/PDFium.framework/PDFium|/System/Library/|/usr/lib/)'; then + printf 'PDFium framework has an unbundled dependency.\n' >&2 + exit 1 +fi + # The UI fonts are embedded in the executable. Ship their OFL notices in the # application bundle as required for redistributed copies of the fonts. font_license_root="$TARGET_BUILD_DIR/${UNLOCALIZED_RESOURCES_FOLDER_PATH:?}/Licenses" diff --git a/platform/ios/project.yml b/platform/ios/project.yml index b13834e3..71dcdad7 100644 --- a/platform/ios/project.yml +++ b/platform/ios/project.yml @@ -10,7 +10,7 @@ targets: FlectarMail: type: application platform: iOS - deploymentTarget: "12.0" + deploymentTarget: "17.0" info: path: Info.plist properties: diff --git a/resources/AppRun b/resources/AppRun index 0ad43772..f4a84b09 100755 --- a/resources/AppRun +++ b/resources/AppRun @@ -85,6 +85,10 @@ fi export MALLOC_ARENA_MAX="${MALLOC_ARENA_MAX:-1}" export MALLOC_TRIM_THRESHOLD_="${MALLOC_TRIM_THRESHOLD_:-65536}" +# Preserve the host search path for external programs such as the browser. +export FLECTAR_MAIL_HOST_LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}" +export FLECTAR_MAIL_BUNDLED=1 + # linuxdeploy places any non-system ELF dependencies here. export LD_LIBRARY_PATH="$app_dir/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # The Linux binary supports Slint's Wayland and X11 winit integrations. Slint diff --git a/resources/debian/control.in b/resources/debian/control.in index 4ba63373..9ed56bed 100644 --- a/resources/debian/control.in +++ b/resources/debian/control.in @@ -5,7 +5,7 @@ Priority: optional Architecture: amd64 Maintainer: Flectar Depends: @DEPENDS@ -Recommends: xdg-desktop-portal +Recommends: xdg-desktop-portal, gnupg, pinentry-gnome3 | pinentry-qt Homepage: https://flectar.com Description: Native Slint mail client Flectar Mail is a native desktop mail client built with Rust and Slint. diff --git a/resources/debian/source-control.in b/resources/debian/source-control.in index 7f5b32df..d91447c8 100644 --- a/resources/debian/source-control.in +++ b/resources/debian/source-control.in @@ -8,7 +8,7 @@ Homepage: https://flectar.com Package: flectar-mail Architecture: amd64 -Depends: ${shlibs:Depends}, libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme +Depends: ${shlibs:Depends}, libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme, xdg-utils Recommends: xdg-desktop-portal Description: Native Slint mail client Flectar Mail is a native desktop mail client built with Rust and Slint. diff --git a/scripts/build-android-release.sh b/scripts/build-android-release.sh index d4bf1c25..61afb62e 100755 --- a/scripts/build-android-release.sh +++ b/scripts/build-android-release.sh @@ -91,6 +91,8 @@ native_lib="$target_dir/aarch64-linux-android/release/libflectar_mail_android.so jni_dir="$target_dir/android/gradle-jni/arm64-v8a" mkdir -p "$jni_dir" cp "$native_lib" "$jni_dir/libflectar_mail_android.so" +python3 "$project_dir/scripts/stage-pdfium.py" android-arm64 "$jni_dir" \ + --cache "$target_dir/pdfium-downloads" "$gradle_command" --no-daemon \ --project-cache-dir "$target_dir/android/gradle-cache" \ diff --git a/scripts/build-android-test-apk.sh b/scripts/build-android-test-apk.sh index ea85ec2b..fa2c7361 100755 --- a/scripts/build-android-test-apk.sh +++ b/scripts/build-android-test-apk.sh @@ -87,6 +87,8 @@ native_lib="$target_dir/aarch64-linux-android/release/libflectar_mail_android.so jni_dir="$target_dir/android/gradle-jni/arm64-v8a" mkdir -p "$jni_dir" cp "$native_lib" "$jni_dir/libflectar_mail_android.so" +python3 "$project_dir/scripts/stage-pdfium.py" android-arm64 "$jni_dir" \ + --cache "$target_dir/pdfium-downloads" export FLECTAR_ANDROID_KEYSTORE="$keystore" export FLECTAR_ANDROID_KEYSTORE_PASSWORD=android diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index a17c7ae3..86f7aeba 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -54,6 +54,7 @@ mkdir -p \ "$app_dir/usr/share/icons/hicolor/scalable/apps" \ "$app_dir/usr/share/metainfo" cp "$project_dir/target/release/flectar-mail" "$app_dir/usr/bin/flectar-mail" +python3 "$project_dir/scripts/stage-pdfium.py" linux-x64 "$app_dir/usr/lib/flectar-mail" app_version="$(python3 "$project_dir/scripts/stage-linux-metadata.py" "$app_dir")" cp "$project_dir/resources/app-icon/flectar-mail-masked-512.png" "$app_dir/usr/share/icons/hicolor/512x512/apps/com.flectar.mail.png" cp "$project_dir/resources/app-icon/flectar-mail-masked.svg" "$app_dir/usr/share/icons/hicolor/scalable/apps/com.flectar.mail.svg" @@ -87,6 +88,10 @@ VERSION="$app_version" APPIMAGE_EXTRACT_AND_RUN="${APPIMAGE_EXTRACT_AND_RUN:-1}" cp "$project_dir/resources/AppRun" "$app_dir/AppRun" chmod +x "$app_dir/AppRun" +# The pinned PDFium runtime depends only on the ordinary glibc runtime. Keep +# its private path intact and verify it after deployment has adjusted the ELF files. +python3 "$project_dir/scripts/test-pdf-preview.py" "$app_dir/usr/bin/flectar-mail" + # AppDir specifies a PNG .DirIcon for file-manager thumbnails. linuxdeploy # prefers the scalable icon for the root entry, so restore the PNG thumbnail # explicitly while retaining the SVG and themed icon copies for integration. diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh index 4b272d5a..c8b899da 100755 --- a/scripts/build-deb.sh +++ b/scripts/build-deb.sh @@ -33,6 +33,8 @@ mkdir -p \ install -m755 "$project_dir/target/release/flectar-mail" \ "$package_root/usr/bin/flectar-mail" +python3 "$project_dir/scripts/stage-pdfium.py" linux-x64 "$package_root/usr/lib/flectar-mail" +python3 "$project_dir/scripts/test-pdf-preview.py" "$package_root/usr/bin/flectar-mail" python3 "$project_dir/scripts/stage-linux-metadata.py" "$package_root" install -m644 "$project_dir/resources/app-icon/flectar-mail-masked-512.png" \ "$package_root/usr/share/icons/hicolor/512x512/apps/com.flectar.mail.png" @@ -55,10 +57,10 @@ install -m644 "$project_dir/resources/fonts/google-sans-flex/README.md" \ cp "$project_dir/resources/debian/source-control.in" "$build_dir/debian/control" shlib_substitution="$( cd "$build_dir" - dpkg-shlibdeps -O -edebian/flectar-mail/usr/bin/flectar-mail + dpkg-shlibdeps -O -edebian/flectar-mail/usr/bin/flectar-mail -edebian/flectar-mail/usr/lib/flectar-mail/libpdfium.so )" runtime_dependencies="${shlib_substitution#shlibs:Depends=}" -runtime_dependencies+=", libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme" +runtime_dependencies+=", libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme, xdg-utils" sed \ -e "s/@VERSION@/$version/g" \ -e "s/@DEPENDS@/$runtime_dependencies/g" \ diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh index 55e18886..dcc2b121 100755 --- a/scripts/package-macos.sh +++ b/scripts/package-macos.sh @@ -19,6 +19,8 @@ mkdir -p \ "$app_dir/Contents/Resources/Licenses/Noto Emoji" cp "$binary_path" "$app_dir/Contents/MacOS/flectar-mail" +python3 "$project_dir/scripts/stage-pdfium.py" mac-arm64 "$app_dir/Contents/MacOS" + icon_source="$project_dir/resources/app-icon/flectar-mail-masked.png" iconset_dir="$output_dir/FlectarMail.iconset" rm -rf "$iconset_dir" @@ -58,6 +60,7 @@ sed \ # An ad-hoc signature makes the CI artifact internally consistent. Distribution # signing and notarization replace it when Apple credentials are configured. codesign --force --deep --sign - "$app_dir" +python3 "$project_dir/scripts/test-pdf-preview.py" "$app_dir/Contents/MacOS/flectar-mail" mkdir -p "$output_dir" ditto -c -k --sequesterRsrc --keepParent "$app_dir" "$output_dir/flectar-mail-macos-arm64.zip" diff --git a/scripts/stage-pdfium.py b/scripts/stage-pdfium.py new file mode 100644 index 00000000..826af404 --- /dev/null +++ b/scripts/stage-pdfium.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Stage a checksum-pinned, non-V8 PDFium runtime for desktop and mobile app packaging. + +No downloads happen at application startup or during cargo build. Release +packaging calls this explicitly; developers can target target/debug. +""" +import argparse +import hashlib +import io +import json +import pathlib +import shutil +import tarfile +import urllib.request + +RELEASE = "chromium/8044" +ASSETS = { + "linux-x64": ("eb142f416aed3a72fc5a02dbd5884868a16cb99dc0cf53e6bdd64afbf67b05f4", "lib/libpdfium.so"), + "mac-arm64": ("61424884d4a7f153b808deba6437848e4400834ce30aaf95d3050da44df8f420", "lib/libpdfium.dylib"), + "android-arm64": ("37686e64fa005484d619550a78805500de4c5a6f4df7aa06d8576145bfbee98f", "lib/libpdfium.so"), + "android-x64": ("e7e3072b8ad6f36ef58db999c2d91a22fa165a904d69385f13db4537c233d736", "lib/libpdfium.so"), + "ios-device-arm64": ("52be735d07b6c498ed8ce68b7f0cb5a209bbe61593c7a86c556756b80abbb938", "lib/libpdfium.dylib"), + "ios-simulator-arm64": ("544537fce5257146e0df501c632d490d1aa95768737770f1912c8e23b1dd238d", "lib/libpdfium.dylib"), + "ios-simulator-x64": ("8d4e33e415e29894aa98481bc9a9162a42039a1672796db14c9fed9cd545c881", "lib/libpdfium.dylib"), + "win-x64": ("78a17d9a5f14467631c26a3ac8741b27a0471ecc05bd6a119b523598160a0537", "bin/pdfium.dll"), +} + + +def stage(target, destination, cache=None): + checksum, library = ASSETS[target] + url = f"https://github.com/bblanchon/pdfium-binaries/releases/download/{RELEASE}/pdfium-{target}.tgz" + cached = cache / f"{checksum}.tgz" if cache else None + if cached and cached.is_file(): + data = cached.read_bytes() + else: + with urllib.request.urlopen(url, timeout=60) as response: + data = response.read(64 * 1024 * 1024 + 1) + if hashlib.sha256(data).hexdigest() != checksum: + raise ValueError("PDFium archive checksum mismatch") + if cached and not cached.is_file(): + cache.mkdir(parents=True, exist_ok=True) + # Concurrent Xcode architecture builds must never observe a partial cache. + import tempfile + with tempfile.NamedTemporaryFile(dir=cache, delete=False) as temp: + temp.write(data) + temporary = pathlib.Path(temp.name) + temporary.replace(cached) + destination.mkdir(parents=True, exist_ok=True) + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + members = {member.name.removeprefix("./"): member for member in archive.getmembers()} + for name, member in members.items(): + if name != library and not name.startswith("licenses/") and name != "LICENSE": + continue + if not member.isfile() or ".." in pathlib.PurePosixPath(name).parts: + raise ValueError("Invalid PDFium archive member") + relative = pathlib.Path(library).name if name == library else pathlib.Path("pdfium-licenses") / name + output = destination / relative + output.parent.mkdir(parents=True, exist_ok=True) + with archive.extractfile(member) as source, output.open("wb") as sink: + shutil.copyfileobj(source, sink) + if not (destination / pathlib.Path(library).name).is_file(): + raise ValueError("PDFium runtime missing from archive") + (destination / "pdfium-licenses" / "build.json").write_text(json.dumps({"release": RELEASE, "sha256": checksum, "url": url}, indent=2) + "\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("target", choices=ASSETS) + parser.add_argument("destination", type=pathlib.Path) + parser.add_argument("--cache", type=pathlib.Path, help="Verified archive cache for repeated packaging builds") + args = parser.parse_args() + stage(args.target, args.destination, args.cache) diff --git a/scripts/test-pdf-packaging.py b/scripts/test-pdf-packaging.py new file mode 100644 index 00000000..fb7f82c0 --- /dev/null +++ b/scripts/test-pdf-packaging.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Offline regressions for PDFium staging and native package validation.""" +import hashlib +import importlib.util +import io +import pathlib +import plistlib +import struct +import sys +import tarfile +import tempfile +import unittest +import zipfile +from unittest.mock import patch + +sys.dont_write_bytecode = True +SCRIPTS = pathlib.Path(__file__).resolve().parent + + +def module(name): + spec = importlib.util.spec_from_file_location(name, SCRIPTS / (name + ".py")) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +stage = module("stage-pdfium") +native = module("verify-android-native") +framework = module("write-pdfium-framework-plist") + + +def archive(files): + result = io.BytesIO() + with tarfile.open(fileobj=result, mode="w:gz") as output: + for name, data in files.items(): + entry = tarfile.TarInfo(name) + entry.size = len(data) + output.addfile(entry, io.BytesIO(data)) + return result.getvalue() + + +def elf(alignment=16384): + data = bytearray(120) + data[:6] = b"\x7fELF\x02\x01" + struct.pack_into(">", b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>"] + for content in (6, 7): + objects.append(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents {content} 0 R >>".encode()) + objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") + objects += [b"<< /Length " + str(len(s)).encode() + b" >>\nstream\n" + s + b"\nendstream" for s in streams] + data = b"%PDF-1.4\n"; offsets = [0] + for i, obj in enumerate(objects, 1): + offsets.append(len(data)); data += f"{i} 0 obj\n".encode() + obj + b"\nendobj\n" + xref = len(data) + data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode() + data += b"".join(f"{offset:010} 00000 n \n".encode() for offset in offsets[1:]) + return data + f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode() + + +def png(path, width, height, pixels): + def chunk(kind, value): + return struct.pack(">I", len(value)) + kind + value + struct.pack(">I", zlib.crc32(kind + value)) + raw = b"".join(b"\0" + pixels[y * width * 4:(y + 1) * width * 4] for y in range(height)) + pathlib.Path(path).write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")) + + +def check(binary, output=None): + data = fixture() + for index, zoom in [(0, 100), (1, 100), (0, 200)]: + start = time.monotonic() + result = subprocess.run([binary, "--render-pdf-page"], input=struct.pack(" 2 else None) diff --git a/scripts/test-release.py b/scripts/test-release.py index 34bbcb22..d8eb50ea 100644 --- a/scripts/test-release.py +++ b/scripts/test-release.py @@ -145,6 +145,19 @@ class ReleaseTests(unittest.TestCase): (root / "Cargo.toml").write_text('[package]\nversion = "0.1.0-alpha.1"\n') (root / "target/release").mkdir(parents=True) shutil.copyfile("/bin/true", root / "target/release/flectar-mail") + # This test checks package metadata using fixture ELFs. Stub only + # the download/renderer helpers in the disposable checkout; actual + # native PDF rendering is exercised by the dedicated smoke tests. + (root / "scripts/stage-pdfium.py").write_text( + "import pathlib, shutil, sys\n" + "destination = pathlib.Path(sys.argv[2])\n" + "destination.mkdir(parents=True, exist_ok=True)\n" + "shutil.copyfile('/bin/true', destination / 'libpdfium.so')\n" + ) + (root / "scripts/test-pdf-preview.py").write_text( + "import pathlib, sys\n" + "assert pathlib.Path(sys.argv[1]).is_file()\n" + ) (root / "bin").mkdir() cargo = root / "bin/cargo" cargo.write_text("#!/bin/sh\nexit 0\n") diff --git a/scripts/verify-android-apk.sh b/scripts/verify-android-apk.sh index 39b4a1db..e59d26bf 100755 --- a/scripts/verify-android-apk.sh +++ b/scripts/verify-android-apk.sh @@ -71,6 +71,8 @@ if [[ "$archive_listing" != *'classes.dex'* ]]; then exit 1 fi +python3 "$project_dir/scripts/verify-android-native.py" "$apk" + signing="$($apksigner verify --verbose --print-certs "$apk")" printf '%s\n' "$signing" signing_args=("$signing_mode") diff --git a/scripts/verify-android-native.py b/scripts/verify-android-native.py new file mode 100644 index 00000000..29885b61 --- /dev/null +++ b/scripts/verify-android-native.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Verify packaged native ABI completeness and 16 KiB ELF load alignment.""" +import argparse +import struct +import zipfile + + +def verify_elf(data, name): + if data[:6] != b"\x7fELF\x02\x01": + raise ValueError(f"{name}: expected little-endian ELF64") + offset = struct.unpack_from(" len(data): + raise ValueError(f"{name}: invalid ELF program headers") + loads = 0 + for i in range(count): + entry = offset + i * entry_size + if struct.unpack_from("