| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "strconv" |
| 6 | ) |
| 7 | |
| 8 | // formatFromForm decodes a formatPatch out of an ordinary HTML form. |
| 9 | // |
| 10 | // The second decoder for one route (this slice's decision D). Both |
| 11 | // converge on a formatPatch and NOTHING is validated here — validate, |
| 12 | // normalize and the maxFormatCells bound all run afterwards, in the |
| 13 | // handler, on whichever patch arrived. If this function checked an |
| 14 | // alignment or clamped a rectangle, the form path and the JSON path |
| 15 | // would be two implementations of the rules and would drift, and the |
| 16 | // drift would only surface when somebody formatted in one view and read |
| 17 | // the result in the other. |
| 18 | // |
| 19 | // **Every toggle is tri-state, and that is not a style choice.** |
| 20 | // formatPatch's fields are pointers precisely so that "leave bold |
| 21 | // alone" and "turn bold off" are different requests — and an unchecked |
| 22 | // checkbox is simply ABSENT from a form submission, indistinguishable |
| 23 | // from a field the form never had. A checkbox-shaped bold control can |
| 24 | // therefore express "make it bold" and can never express "make it not |
| 25 | // bold", which is half a control. So each toggle posts an explicit |
| 26 | // word: "set", "clear", or nothing at all. |
| 27 | func formatFromForm(r *http.Request) (formatPatch, bool) { |
| 28 | var p formatPatch |
| 29 | |
| 30 | // ParseForm's error is checked, and that is not defensive noise. |
| 31 | // r.FormValue swallows it — including the one MaxBytesReader raises |
| 32 | // on an oversized body — and Go still populates r.Form from the URL |
| 33 | // QUERY. So without this, a post whose body was refused had its |
| 34 | // patch read from the query string instead and the write landed: |
| 35 | // the cap was enforced on the body and bypassed by moving the |
| 36 | // fields. sizeFromForm has always checked; this one had not, so the |
| 37 | // two decoders disagreed about what a malformed form is. Found by |
| 38 | // an independent review (Codex, 2026-09-05). |
| 39 | if err := r.ParseForm(); err != nil { |
| 40 | return p, false |
| 41 | } |
| 42 | |
| 43 | rect := []struct { |
| 44 | name string |
| 45 | into *int |
| 46 | }{{"r1", &p.R1}, {"c1", &p.C1}, {"r2", &p.R2}, {"c2", &p.C2}} |
| 47 | for _, f := range rect { |
| 48 | v, err := strconv.Atoi(r.FormValue(f.name)) |
| 49 | if err != nil { |
| 50 | return p, false |
| 51 | } |
| 52 | *f.into = v |
| 53 | } |
| 54 | |
| 55 | if b, ok := triState(r.FormValue("bold")); ok { |
| 56 | p.Bold = &b |
| 57 | } |
| 58 | if b, ok := triState(r.FormValue("italic")); ok { |
| 59 | p.Italic = &b |
| 60 | } |
| 61 | // The three string fields carry their own "leave alone", which is |
| 62 | // the empty submission — but "" is also a MEANINGFUL value for |
| 63 | // align and numfmt (it clears back to the engine's own choice), so |
| 64 | // they cannot use the same convention as the booleans. A separate |
| 65 | // word does the job: the control posts "keep" when it is not |
| 66 | // changing anything. |
| 67 | for _, f := range []struct { |
| 68 | name string |
| 69 | into **string |
| 70 | }{{"align", &p.Align}, {"numfmt", &p.NumFmt}, {"fill", &p.Fill}, {"ink", &p.Ink}} { |
| 71 | v := r.FormValue(f.name) |
| 72 | if v == "keep" { |
| 73 | continue |
| 74 | } |
| 75 | if _, present := r.Form[f.name]; !present { |
| 76 | continue |
| 77 | } |
| 78 | val := v |
| 79 | *f.into = &val |
| 80 | } |
| 81 | return p, true |
| 82 | } |
| 83 | |
| 84 | // triState reads a boolean toggle. "set" and "clear" are the two |
| 85 | // changes; anything else — including the empty string a "leave alone" |
| 86 | // option posts, and including a value nobody expected — leaves the |
| 87 | // field absent. |
| 88 | // |
| 89 | // Deliberately not accepting "true"/"false"/"on": "on" is what a |
| 90 | // checkbox posts, and quietly accepting it would let somebody build the |
| 91 | // checkbox-shaped control this function exists to rule out, and have it |
| 92 | // appear to work for exactly the half of its job that it can do. |
| 93 | func triState(v string) (bool, bool) { |
| 94 | switch v { |
| 95 | case "set": |
| 96 | return true, true |
| 97 | case "clear": |
| 98 | return false, true |
| 99 | } |
| 100 | return false, false |
| 101 | } |
| 102 | |