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)

This commit is contained in:
2026-10-20 21:37:07 +02:00
parent b430b35633
commit 02cb424d5c
8 changed files with 53 additions and 11 deletions
+8
View File
@@ -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 `<sha256>.pdf` and an upload as `uploads/<id>.<ext>`.
### The device process
`brother device` runs as `brother-device` with a bus account of its own (the own secret `broker-device`), never a
+22 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -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/<upload>.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{
+8 -2
View File
@@ -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)
}
+5 -4
View File
@@ -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
)
+1 -1
View File
@@ -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
}
+7 -2
View File
@@ -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",