| 1 | package launchctx |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "io" |
| 9 | "os" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | ) |
| 13 | |
| 14 | func launchFixture(t *testing.T) ([]byte, LaunchContext) { |
| 15 | t.Helper() |
| 16 | raw, err := os.ReadFile("testdata/launch-v2.json") |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | c, err := DecodeLaunch(raw) |
| 21 | if err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | return raw, c |
| 25 | } |
| 26 | func TestLaunchExactIntegersAndIndependentCheckpointVector(t *testing.T) { |
| 27 | _, c := launchFixture(t) |
| 28 | if c.DatabaseDevice != ^uint64(0) || c.DatabaseInode != 9007199254740993 || c.GuardGeneration != 9223372036854775807 { |
| 29 | t.Fatal("integer precision lost") |
| 30 | } |
| 31 | raw, err := EncodeLaunch(c) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | again, err := ReadLaunch(bytes.NewReader(raw)) |
| 36 | if err != nil || again.DatabaseDevice != c.DatabaseDevice || again.DatabaseInode != c.DatabaseInode { |
| 37 | t.Fatal("round trip lost identity", err) |
| 38 | } |
| 39 | binding, err := os.ReadFile("testdata/launch-v2-binding.json") |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | if !bytes.Contains(binding, []byte(`18446744073709551615`)) || !bytes.Contains(binding, []byte(`\u003chome\u003e\u0026`)) { |
| 44 | t.Fatal("independent vector lacks precision/escaping cases") |
| 45 | } |
| 46 | if !bytes.Equal(binding, c.checkpointBytes()) { |
| 47 | t.Fatal("Go binding differs from independent Python vector") |
| 48 | } |
| 49 | digest := sha256.Sum256(binding) |
| 50 | if hex.EncodeToString(digest[:]) != c.ClosedCheckpoint.EvidenceSHA256 { |
| 51 | t.Fatal("independent vector digest mismatch") |
| 52 | } |
| 53 | c.Version = 1 |
| 54 | c.ExecutionKind = "" |
| 55 | c.OperationID = "" |
| 56 | c.ExpectedStateSHA256 = "" |
| 57 | c.ClosedCheckpoint = nil |
| 58 | raw, err = EncodeLaunch(c) |
| 59 | if err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | if _, err := DecodeLaunch(raw); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | c.ExecutionKind = "compatibility-check" |
| 66 | if _, err := EncodeLaunch(c); err == nil { |
| 67 | t.Fatal("v1 accepted checker fields") |
| 68 | } |
| 69 | } |
| 70 | func TestLaunchRefusesEachMissingOrAmbiguousField(t *testing.T) { |
| 71 | raw, _ := launchFixture(t) |
| 72 | var fields map[string]json.RawMessage |
| 73 | if err := json.Unmarshal(raw, &fields); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | for key := range fields { |
| 77 | t.Run(key, func(t *testing.T) { |
| 78 | var f map[string]json.RawMessage |
| 79 | json.Unmarshal(raw, &f) |
| 80 | delete(f, key) |
| 81 | bad, _ := json.Marshal(f) |
| 82 | if _, err := DecodeLaunch(bad); err == nil { |
| 83 | t.Fatal("missing field accepted") |
| 84 | } |
| 85 | f[strings.ToUpper(key)] = fields[key] |
| 86 | bad, _ = json.Marshal(f) |
| 87 | if _, err := DecodeLaunch(bad); err == nil { |
| 88 | t.Fatal("case alias accepted") |
| 89 | } |
| 90 | f[key] = json.RawMessage("null") |
| 91 | delete(f, strings.ToUpper(key)) |
| 92 | bad, _ = json.Marshal(f) |
| 93 | if _, err := DecodeLaunch(bad); err == nil { |
| 94 | t.Fatal("null accepted") |
| 95 | } |
| 96 | }) |
| 97 | } |
| 98 | for _, bad := range [][]byte{append(append([]byte{}, raw...), []byte(` {}`)...), append([]byte(`{"version":2,`), bytes.TrimSpace(raw)[1:]...), bytes.Replace(raw, []byte(`"database_inode": 9007199254740993`), []byte(`"database_inode": 18446744073709551616`), 1), bytes.Replace(raw, []byte(`"guard_generation": 9223372036854775807`), []byte(`"guard_generation": 1e3`), 1), bytes.Replace(raw, []byte(`"database_inode": 9007199254740993`), []byte(`"database_inode": 1.0`), 1), bytes.Replace(raw, []byte(`"version": 2`), []byte(`"version": 2.0`), 1), bytes.Replace(raw, []byte(`"main_length": 268435456`), []byte(`"main_length": 268435457`), 1)} { |
| 99 | if _, err := DecodeLaunch(bad); err == nil { |
| 100 | t.Fatalf("malformed context accepted: %s", bad) |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | func TestLaunchCheckpointCannotBeTransplanted(t *testing.T) { |
| 105 | _, c := launchFixture(t) |
| 106 | raw, _ := EncodeLaunch(c) |
| 107 | var fields map[string]json.RawMessage |
| 108 | json.Unmarshal(raw, &fields) |
| 109 | for key := range fields { |
| 110 | if key == "closed_checkpoint" || key == "version" || key == "execution_kind" { |
| 111 | continue |
| 112 | } |
| 113 | t.Run(key, func(t *testing.T) { |
| 114 | var f map[string]json.RawMessage |
| 115 | json.Unmarshal(raw, &f) |
| 116 | var value any |
| 117 | switch key { |
| 118 | case "account": |
| 119 | value = "abc" |
| 120 | case "app": |
| 121 | value = "docs" |
| 122 | case "host": |
| 123 | value = "other.bds.oncarlos.com" |
| 124 | case "request_id", "attempt_id", "operation_id": |
| 125 | value = strings.Repeat("A", 43) |
| 126 | case "database_path": |
| 127 | value = "/other/home.db" |
| 128 | case "execution_deadline": |
| 129 | value = "2026-09-11T21:00:00Z" |
| 130 | case "guard_generation", "database_device", "database_inode": |
| 131 | value = 2 |
| 132 | default: |
| 133 | value = strings.Repeat("a", 64) |
| 134 | } |
| 135 | f[key], _ = json.Marshal(value) |
| 136 | bad, _ := json.Marshal(f) |
| 137 | if _, err := DecodeLaunch(bad); err == nil { |
| 138 | t.Fatal("changed valid binding accepted with old evidence") |
| 139 | } |
| 140 | var changed LaunchContext |
| 141 | json.Unmarshal(bad, &changed) |
| 142 | changed.ClosedCheckpoint.EvidenceSHA256 = changed.checkpointDigest() |
| 143 | if _, err := EncodeLaunch(changed); err != nil { |
| 144 | t.Fatal("mutation was not valid independent of its evidence", err) |
| 145 | } |
| 146 | }) |
| 147 | } |
| 148 | for _, mutate := range []func(*ClosedCheckpoint){func(p *ClosedCheckpoint) { p.PreparationID = strings.Repeat("A", 43) }, func(p *ClosedCheckpoint) { p.MainLength-- }, func(p *ClosedCheckpoint) { p.MainSHA256 = strings.Repeat("a", 64) }, func(p *ClosedCheckpoint) { p.CompletedAt = "2026-09-11T19:59:58Z" }} { |
| 149 | _, v := launchFixture(t) |
| 150 | mutate(v.ClosedCheckpoint) |
| 151 | b, _ := json.Marshal(v) |
| 152 | if _, err := DecodeLaunch(b); err == nil { |
| 153 | t.Fatal("changed preparation accepted") |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | func TestLaunchSizeUTF8AndSurrogates(t *testing.T) { |
| 158 | _, c := launchFixture(t) |
| 159 | c.Version = 1 |
| 160 | c.ExecutionKind = "" |
| 161 | c.OperationID = "" |
| 162 | c.ExpectedStateSHA256 = "" |
| 163 | c.ClosedCheckpoint = nil |
| 164 | raw, err := EncodeLaunch(c) |
| 165 | if err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | exact := append(bytes.Repeat([]byte(" "), MaxLaunchBytes-len(raw)), raw...) |
| 169 | if _, err := DecodeLaunch(exact); err != nil { |
| 170 | t.Fatal("exact bound refused", err) |
| 171 | } |
| 172 | r := bytes.NewReader(append(exact, []byte("xx")...)) |
| 173 | if _, err := ReadLaunch(r); err == nil || r.Len() != 1 { |
| 174 | t.Fatal("overflow bound", err, r.Len()) |
| 175 | } |
| 176 | for _, path := range []string{`"/bad\ud800"`, `"/bad\udc00"`, `"/bad\ud800\u0001"`, `"/bad` + string([]byte{0xff}) + `"`} { |
| 177 | oldPath, _ := json.Marshal(c.DatabasePath) |
| 178 | bad := bytes.Replace(raw, oldPath, []byte(path), 1) |
| 179 | if bytes.Equal(bad, raw) { |
| 180 | t.Fatal("mutation missed path") |
| 181 | } |
| 182 | |
| 183 | if _, err := DecodeLaunch(bad); err == nil { |
| 184 | t.Fatal("invalid string accepted") |
| 185 | } |
| 186 | } |
| 187 | c.DatabasePath = "/valid/😀/�/quoted\".db" |
| 188 | raw, err = EncodeLaunch(c) |
| 189 | if err != nil { |
| 190 | t.Fatal(err) |
| 191 | } |
| 192 | raw = bytes.ReplaceAll(raw, []byte("😀"), []byte(`\ud83d\ude00`)) |
| 193 | if _, err := DecodeLaunch(raw); err != nil { |
| 194 | t.Fatal("valid surrogate pair refused", err) |
| 195 | } |
| 196 | if _, err := ReadLaunch(io.MultiReader(bytes.NewReader(raw), errReader{})); err == nil { |
| 197 | t.Fatal("read error accepted") |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | type errReader struct{} |
| 202 | |
| 203 | func (errReader) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF } |
| 204 | |
| 205 | func TestNestedLaunchFieldsAreClosed(t *testing.T) { |
| 206 | raw, _ := launchFixture(t) |
| 207 | var top, fields map[string]json.RawMessage |
| 208 | json.Unmarshal(raw, &top) |
| 209 | original := top["closed_checkpoint"] |
| 210 | json.Unmarshal(original, &fields) |
| 211 | refuse := func(b json.RawMessage) { |
| 212 | t.Helper() |
| 213 | top["closed_checkpoint"] = b |
| 214 | bad, _ := json.Marshal(top) |
| 215 | if _, err := DecodeLaunch(bad); err == nil { |
| 216 | t.Fatal("ambiguous checkpoint accepted") |
| 217 | } |
| 218 | } |
| 219 | for key, value := range fields { |
| 220 | t.Run(key, func(t *testing.T) { |
| 221 | var f map[string]json.RawMessage |
| 222 | json.Unmarshal(original, &f) |
| 223 | delete(f, key) |
| 224 | bad, _ := json.Marshal(f) |
| 225 | refuse(bad) |
| 226 | f[strings.ToUpper(key)] = value |
| 227 | bad, _ = json.Marshal(f) |
| 228 | refuse(bad) |
| 229 | delete(f, strings.ToUpper(key)) |
| 230 | f[key] = json.RawMessage("null") |
| 231 | bad, _ = json.Marshal(f) |
| 232 | refuse(bad) |
| 233 | prefix, _ := json.Marshal(key) |
| 234 | dup := append(append(append([]byte{'{'}, prefix...), ':'), value...) |
| 235 | dup = append(dup, ',') |
| 236 | dup = append(dup, bytes.TrimSpace(original)[1:]...) |
| 237 | refuse(dup) |
| 238 | }) |
| 239 | } |
| 240 | var f map[string]json.RawMessage |
| 241 | json.Unmarshal(original, &f) |
| 242 | f["extra"] = json.RawMessage("1") |
| 243 | bad, _ := json.Marshal(f) |
| 244 | refuse(bad) |
| 245 | } |
| 246 | func TestLaunchNumericAndTimestampLexicalForms(t *testing.T) { |
| 247 | raw, c := launchFixture(t) |
| 248 | for _, key := range []string{"version", "guard_generation", "database_device", "database_inode"} { |
| 249 | for _, value := range []string{"0", "-0", "-1", "1.0", "1e0", "18446744073709551616"} { |
| 250 | var f map[string]json.RawMessage |
| 251 | json.Unmarshal(raw, &f) |
| 252 | f[key] = json.RawMessage(value) |
| 253 | bad, _ := json.Marshal(f) |
| 254 | if _, err := DecodeLaunch(bad); err == nil { |
| 255 | t.Fatal("noncanonical integer accepted", key, value) |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | for _, stamp := range []string{"2026-09-11T20:00:00+00:00", "2026-09-11T20:00:00.120Z", "2026-09-11t20:00:00Z", "2026-09-11T20:00:00z"} { |
| 260 | v := c |
| 261 | v.ExecutionDeadline = stamp |
| 262 | v.ClosedCheckpoint = &ClosedCheckpoint{} |
| 263 | *v.ClosedCheckpoint = *c.ClosedCheckpoint |
| 264 | v.ClosedCheckpoint.EvidenceSHA256 = v.checkpointDigest() |
| 265 | if _, err := EncodeLaunch(v); err == nil { |
| 266 | t.Fatal("noncanonical timestamp accepted", stamp) |
| 267 | } |
| 268 | } |
| 269 | c.ClosedCheckpoint.CompletedAt = c.ExecutionDeadline |
| 270 | c.ClosedCheckpoint.EvidenceSHA256 = c.checkpointDigest() |
| 271 | if _, err := EncodeLaunch(c); err != nil { |
| 272 | t.Fatal("equal completion/deadline refused", err) |
| 273 | } |
| 274 | } |
| 275 | |