1
0
Files
kobopatch-webui/kobopatch-wasm/integration_test.go
Nico Verbruggen 1c8e58e34d Update software URLs
Since the output of the modded files is identical for each version, the
fact that these were swapped for Clara/Libra did not matter, but it's
been amended now.

I've added patches for older devices on 4.38, but this isn't currently
usable in the UI as I've not set up the `downloads.json` file with
correct and up-to-date URLs for these devices.

I'm not even sure I want to actually support this, as the SD card
storage in these older devices is less reliable, which means that
applying custom patches is a little bit more risky in my eyes.
2026-03-26 11:38:25 +01:00

176 lines
4.6 KiB
Go

//go:build js && wasm
package main
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"crypto/sha1"
"fmt"
"io"
"os"
"strings"
"testing"
)
// TestIntegrationPatch runs the full patching pipeline with real patch files
// and validates SHA1 checksums of the patched binaries.
//
// Requires the firmware zip to be present at testdata/kobo-update-4.45.23646.zip
// (or the path set via FIRMWARE_ZIP env var). Run test-integration.sh to download
// the firmware and execute this test.
func TestIntegrationPatch(t *testing.T) {
firmwarePath := os.Getenv("FIRMWARE_ZIP")
if firmwarePath == "" {
firmwarePath = "testdata/kobo-update-4.45.23646.zip"
}
firmwareZip, err := os.ReadFile(firmwarePath)
if err != nil {
t.Skipf("firmware zip not available at %s (run test-integration.sh to download): %v", firmwarePath, err)
}
// Read patch files from the patches zip.
patchesZipPath := "../web/src/patches/patches_4.45.zip"
patchesZip, err := os.ReadFile(patchesZipPath)
if err != nil {
t.Fatalf("could not read patches zip: %v", err)
}
patchFiles, err := extractPatchFiles(patchesZip)
if err != nil {
t.Fatalf("could not extract patch files: %v", err)
}
// Config: all patches at their defaults, with one override enabled.
configYAML := `
version: 4.45.23646
in: unused
out: unused
log: unused
patches:
src/nickel.yaml: usr/local/Kobo/nickel
src/nickel_custom.yaml: usr/local/Kobo/nickel
src/libadobe.so.yaml: usr/local/Kobo/libadobe.so
src/libnickel.so.1.0.0.yaml: usr/local/Kobo/libnickel.so.1.0.0
src/librmsdk.so.1.0.0.yaml: usr/local/Kobo/librmsdk.so.1.0.0
src/cloud_sync.yaml: usr/local/Kobo/libnickel.so.1.0.0
overrides:
src/nickel.yaml:
"Remove footer (row3) on new home screen": yes
`
var logMessages []string
progress := func(msg string) {
logMessages = append(logMessages, msg)
}
result, err := patchFirmware([]byte(configYAML), firmwareZip, patchFiles, progress)
if err != nil {
t.Fatalf("patchFirmware failed: %v", err)
}
if len(result.tgzBytes) == 0 {
t.Fatal("patchFirmware returned empty tgz")
}
// Expected SHA1 checksums for Kobo Libra Color, firmware 4.45.23646,
// with only "Remove footer (row3) on new home screen" enabled.
expectedSHA1 := map[string]string{
"usr/local/Kobo/libnickel.so.1.0.0": "ef64782895a47ac85f0829f06fffa4816d23512d",
"usr/local/Kobo/nickel": "80a607bac515457a6864be8be831df631a01005c",
"usr/local/Kobo/libadobe.so": "02dc99c71c4fef75401cd49ddc2e63f928a126e1",
"usr/local/Kobo/librmsdk.so.1.0.0": "e3819260c9fc539a53db47e9d3fe600ec11633d5",
}
// Extract the output tgz and check SHA1 of each patched binary.
actualSHA1, err := extractTgzSHA1(result.tgzBytes)
if err != nil {
t.Fatalf("could not extract output tgz: %v", err)
}
for name, expected := range expectedSHA1 {
actual, ok := actualSHA1[name]
if !ok {
// Try with ./ prefix (tar entries may vary).
actual, ok = actualSHA1["./"+name]
}
if !ok {
t.Errorf("missing binary in output: %s", name)
continue
}
if actual != expected {
t.Errorf("SHA1 mismatch for %s:\n expected: %s\n actual: %s", name, expected, actual)
} else {
t.Logf("OK %s = %s", name, actual)
}
}
t.Logf("output tgz size: %d bytes", len(result.tgzBytes))
t.Logf("log output:\n%s", result.log)
}
// extractPatchFiles reads a patches zip and returns a map of filename -> contents
// for all src/*.yaml files.
func extractPatchFiles(zipData []byte) (map[string][]byte, error) {
r, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, err
}
files := make(map[string][]byte)
for _, f := range r.File {
if !strings.HasPrefix(f.Name, "src/") || !strings.HasSuffix(f.Name, ".yaml") {
continue
}
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("open %s: %w", f.Name, err)
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("read %s: %w", f.Name, err)
}
files[f.Name] = data
}
return files, nil
}
// extractTgzSHA1 reads a tgz and returns a map of entry name -> SHA1 hex string.
func extractTgzSHA1(tgzData []byte) (map[string]string, error) {
gr, err := gzip.NewReader(bytes.NewReader(tgzData))
if err != nil {
return nil, err
}
defer gr.Close()
tr := tar.NewReader(gr)
sums := make(map[string]string)
for {
h, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if h.Typeflag != tar.TypeReg {
continue
}
hasher := sha1.New()
if _, err := io.Copy(hasher, tr); err != nil {
return nil, fmt.Errorf("hash %s: %w", h.Name, err)
}
sums[h.Name] = fmt.Sprintf("%x", hasher.Sum(nil))
}
return sums, nil
}