From 02cb424d5c3b91ddb83d744a48254ea0359f9ffa Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 21:37:07 +0200 Subject: [PATCH] brother: emits declared in the module's own list too, parts of 700 KiB at most, a duplicates window on device-asks, and the built binary ignored (issue 1453) --- .gitignore | 1 + modules/brother/README.md | 8 +++++++ modules/brother/cmd/brother/documents_test.go | 23 ++++++++++++++++++- modules/brother/cmd/brother/main.go | 2 +- modules/brother/cmd/brother/manifest_test.go | 10 ++++++-- modules/brother/cmd/brother/parts.go | 9 ++++---- modules/brother/cmd/brother/printer.go | 2 +- modules/brother/module.json | 9 ++++++-- 8 files changed, 53 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index ea861d09..ab0142b4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ modules/mesh-files/cmd/mesh-files/mesh-files # The provisioner client built in place (issue 671). modules/mesh-provisioner-client/cmd/mesh-provisioner-client/mesh-provisioner-client +modules/brother/cmd/brother/brother diff --git a/modules/brother/README.md b/modules/brother/README.md index 439bebc6..50089e55 100644 --- a/modules/brother/README.md +++ b/modules/brother/README.md @@ -79,6 +79,14 @@ it for the device process alone; the tools' bundle is given no such name, and `a no tool file imports the package that reads it. While it is not given, `printer.settings` answers that the secret is not given, and the printer is not contacted. +### Documents in parts + +The bus refuses a message over 1 MiB, so a document crosses it in parts of at most 700 KiB (both the size this module +cuts and the most it accepts; ADR 0468 clause 3 says 768 KiB, which is 1 MiB of base64 before the message around it), +counted from 1, each carried base64-encoded in JSON. A caller joins the parts and checks the SHA-256. The bucket +(`s3-bucket`, from `minio`) is spoken to by the device process alone, from `store.json` (owner `brother-device`, mode +0600), with the S3 client mesh-mail uses; a scan is kept once as `.pdf` and an upload as `uploads/.`. + ### The device process `brother device` runs as `brother-device` with a bus account of its own (the own secret `broker-device`), never a diff --git a/modules/brother/cmd/brother/documents_test.go b/modules/brother/cmd/brother/documents_test.go index fae36de4..ae111a76 100644 --- a/modules/brother/cmd/brother/documents_test.go +++ b/modules/brother/cmd/brother/documents_test.go @@ -24,7 +24,7 @@ func pdfOf(size int) []byte { return b } -// 1453/AC10: a document is cut into parts of at most PartSize, under the 768 KiB limit, and the parts joined in +// 1453/AC10: a document is cut into parts of at most PartSize, under the limit, and the parts joined in // order are the document, with the SHA-256 checked; a wrong SHA-256 or a part missing is refused; a part out of // range is refused naming the range. func TestADocumentIsCutIntoPartsAndJoinedWithItsSha256Checked(t *testing.T) { @@ -438,3 +438,24 @@ func TestAScanAndAPrintAreAcknowledgedBeforeTheWork(t *testing.T) { t.Errorf("settings: early called %d times, action %v", calls, act) } } + +// 1453/AC11: a part of 700 KiB is taken and one of a byte more is refused, by the module's own message naming +// 700 KiB, before it is queued; the limit and the size the module cuts are one. +func TestAPartOverSevenHundredKiBIsRefusedByTheModulesOwnMessage(t *testing.T) { + if MaxPart != PartSize || PartSize != 700<<10 { + t.Fatalf("MaxPart %d, PartSize %d: both are 700 KiB", MaxPart, PartSize) + } + r := newRig(t, `{}`, "pw") + big := make([]byte, 700<<10+1) + _, err := upload(t, r, "over", big, sha256Hex(big), 1, 1, big) + if err == nil || !strings.Contains(err.Error(), "700 KiB") { + t.Fatalf("a part of 700 KiB + 1 byte: error = %v, want the module's own message naming 700 KiB", err) + } + if r.bus.asks() != 0 { + t.Errorf("an oversized part was queued") + } + ok := pdfOf(700 << 10) + if _, err := upload(t, r, "exact", ok, sha256Hex(ok), 1, 1, ok); err != nil { + t.Errorf("a part of exactly 700 KiB: %v", err) + } +} diff --git a/modules/brother/cmd/brother/main.go b/modules/brother/cmd/brother/main.go index e463b621..41a75d78 100644 --- a/modules/brother/cmd/brother/main.go +++ b/modules/brother/cmd/brother/main.go @@ -101,7 +101,7 @@ func tools(settingsFile string, now func() time.Time) []stdio.Tool { { Name: "printer.upload", Description: "Send one part of a document to the device process, to be printed later (printer.print with the upload id). " + - "Cut the document into parts of at most 700 KiB (768 KiB is the limit), number them from 1, and send each with the " + + "Cut the document into parts of at most 700 KiB (700 KiB is the limit), number them from 1, and send each with the " + "whole document's SHA-256. When the last part has come and the joined SHA-256 matches, the document is stored as " + "uploads/.pdf or .jpg and the answer says complete; a wrong SHA-256 stores nothing. PDF and JPEG only.", Input: map[string]any{"type": "object", "required": []string{"upload", "part", "parts", "sha256", "data"}, "properties": map[string]any{ diff --git a/modules/brother/cmd/brother/manifest_test.go b/modules/brother/cmd/brother/manifest_test.go index ada6b1c3..425ba98d 100644 --- a/modules/brother/cmd/brother/manifest_test.go +++ b/modules/brother/cmd/brother/manifest_test.go @@ -64,8 +64,11 @@ func TestTheScanFolderIsNoManifestResource(t *testing.T) { // 1440/AC9: the event the code emits is the one the manifest declares, and the tools it serves are named. func TestTheManifestDeclaresWhatTheCodeServes(t *testing.T) { m := readManifest(t) - if len(m.Emits) != 0 { - t.Errorf("the module's own emits = %v: scan.added is said by the device process, whose bus block declares it", m.Emits) + // Declared in both places, as mesh-mail declares its sender's events: the module's own, and the device + // process's bus block (the controller refuses a process's emit the module does not declare as its own, + // ADR 0436 ยง1). + if !reflect.DeepEqual(m.Emits, []string{EventAdded}) { + t.Errorf("the module's own emits = %v, want [%s]", m.Emits, EventAdded) } bus, _ := resource(t, m, "device")["bus"].(map[string]any) if !reflect.DeepEqual(bus["emits"], []any{EventAdded}) { @@ -312,6 +315,9 @@ func TestThePrinterAsksStreamIsAWorkQueue(t *testing.T) { !reflect.DeepEqual(st["events"], []any{EventAsked}) { t.Errorf("stream = %v", st) } + if st["duplicates"] != float64(24) { + t.Errorf("the stream has no duplicates window like mail-sends': %v", st["duplicates"]) + } if st["max-age-days"] == nil || st["max-mib"] == nil { t.Errorf("the stream is bounded: %v", st) } diff --git a/modules/brother/cmd/brother/parts.go b/modules/brother/cmd/brother/parts.go index 4fa4d983..114e5df0 100644 --- a/modules/brother/cmd/brother/parts.go +++ b/modules/brother/cmd/brother/parts.go @@ -9,11 +9,12 @@ import ( // Documents cross the bus in parts (novox/hq ADR 0468 clause 3): the bus refuses a message over 1 MiB, and a // part travels base64-encoded inside a JSON message. const ( - // MaxPart is the most raw bytes a part may hold; a part of more is refused. - MaxPart = 768 << 10 - // PartSize is how large the parts this module cuts are: below MaxPart, so that the part's base64 text and - // the message around it stay under the bus's 1 MiB. + // PartSize is how large the parts this module cuts are, and MaxPart the most raw bytes a part may hold; a part + // of more is refused. They are one: 700 KiB, so that a part's base64 text (about 933 KB) and the message around + // it stay under the bus's 1 MiB. (ADR 0468 clause 3 says at most 768 KiB, which is 1 MiB of base64 before the + // message around it, and so over the bus's limit.) PartSize = 700 << 10 + MaxPart = PartSize // MostParts bounds how many parts one document has (a 100 MiB document is the most). MostParts = 150 ) diff --git a/modules/brother/cmd/brother/printer.go b/modules/brother/cmd/brother/printer.go index b6cf5b02..a755b937 100644 --- a/modules/brother/cmd/brother/printer.go +++ b/modules/brother/cmd/brother/printer.go @@ -209,7 +209,7 @@ func checkUpload(a Ask) error { return fmt.Errorf("data is not base64: %v", err) } if len(raw) > MaxPart { - return fmt.Errorf("a part is at most %d bytes (768 KiB), not %d", MaxPart, len(raw)) + return fmt.Errorf("a part is at most %d bytes (700 KiB), not %d", MaxPart, len(raw)) } return nil } diff --git a/modules/brother/module.json b/modules/brother/module.json index 0d2e0432..023e2d12 100644 --- a/modules/brother/module.json +++ b/modules/brother/module.json @@ -89,7 +89,8 @@ "max-age-days": 1, "max-mib": 8, "retention": "work-queue", - "max-deliveries": 3 + "max-deliveries": 3, + "duplicates": 24 } ], "state": [ @@ -142,6 +143,9 @@ ] } ], + "emits": [ + "scan.added" + ], "tools": [ "scanner.scan", "scanner.status", @@ -183,7 +187,8 @@ "crop" ], "merge": "json", - "template": "files/settings.json" + "template": "files/settings.json", + "trusted": true }, { "id": "store",