| // Copyright 2026 The BoringSSL Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package runner | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "strconv" | |
| "sync" | |
| "time" | |
| "golang.org/x/crypto/cryptobyte" | |
| ) | |
| // hintTracesSubdir is the subdirectory, under the directory named by | |
| // -hint-traces-dir, where hint traces are read and written. | |
| const hintTracesSubdir = "hint-traces" | |
| // hintTracesFile is the name of the file, within hintTracesSubdir, containing | |
| // the serialized hint traces. | |
| const hintTracesFile = "hint_traces.json" | |
| // hintTrace records the inputs and outputs of a handshake hints test run. | |
| type hintTrace struct { | |
| Connections []connectionTrace `json:"connections"` | |
| ConfigFlags []string `json:"config_flags"` | |
| Date string `json:"date"` | |
| } | |
| // connectionTrace is the per-connection data to record in traces for handshake | |
| // hints tests. | |
| type connectionTrace struct { | |
| ClientHello []byte `json:"client_hello"` | |
| HandshakeHint []byte `json:"handshake_hint"` | |
| } | |
| // A hintTraceRecorder collects hint traces from -Hints tests and serializes | |
| // them to a directory. Its methods may be called concurrently from multiple | |
| // worker goroutines. | |
| type hintTraceRecorder struct { | |
| // dir is the directory in which to write the trace files. | |
| dir string | |
| mu sync.Mutex | |
| traces map[string]*hintTrace | |
| } | |
| // newHintTraceRecorder returns a new, empty recorder which reads and writes | |
| // traces under dir. | |
| func newHintTraceRecorder(dir string) *hintTraceRecorder { | |
| return &hintTraceRecorder{ | |
| dir: filepath.Join(dir, hintTracesSubdir), | |
| traces: make(map[string]*hintTrace), | |
| } | |
| } | |
| // filePrefix returns the prefix of the paths of the per-connection files | |
| // written by the shim for testBaseName. | |
| func (r *hintTraceRecorder) filePrefix(testBaseName string) string { | |
| return filepath.Join(r.dir, testBaseName) | |
| } | |
| // prepare creates the trace directory and returns the prefix to pass to the | |
| // shim's -write-hint-trace flag for testBaseName. | |
| func (r *hintTraceRecorder) prepare(testBaseName string) (string, error) { | |
| if err := os.MkdirAll(r.dir, 0755); err != nil { | |
| return "", err | |
| } | |
| return r.filePrefix(testBaseName), nil | |
| } | |
| // cleanup removes the per-connection files the shim wrote for testBaseName. | |
| func (r *hintTraceRecorder) cleanup(testBaseName string, numConnections int) { | |
| prefix := r.filePrefix(testBaseName) | |
| for i := 0; i < numConnections; i++ { | |
| os.Remove(prefix + strconv.Itoa(i)) | |
| } | |
| } | |
| // readConnections reads the per-connection files the shim wrote for | |
| // testBaseName. Connections whose files are missing are skipped. | |
| func (r *hintTraceRecorder) readConnections(testBaseName string, numConnections int) ([]connectionTrace, error) { | |
| prefix := r.filePrefix(testBaseName) | |
| var connections []connectionTrace | |
| for i := 0; i < numConnections; i++ { | |
| connTrace, err := readConnectionTrace(prefix, i) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if connTrace != nil { | |
| connections = append(connections, *connTrace) | |
| } | |
| } | |
| return connections, nil | |
| } | |
| // readConnectionTrace reads the raw data for a single connection, written as a | |
| // binary file by the shim. It returns nil if the file does not exist. | |
| func readConnectionTrace(hintPathPrefix string, connIndex int) (*connectionTrace, error) { | |
| tracePath := hintPathPrefix + strconv.Itoa(connIndex) | |
| traceData, err := os.ReadFile(tracePath) | |
| if err != nil { | |
| if os.IsNotExist(err) { | |
| return nil, nil | |
| } | |
| return nil, fmt.Errorf("reading %s: %w", tracePath, err) | |
| } | |
| data := cryptobyte.String(traceData) | |
| var clientHello, hint []byte | |
| if !readUint24LengthPrefixedBytes(&data, &clientHello) || len(clientHello) == 0 { | |
| return nil, fmt.Errorf("reading ClientHello for %s", tracePath) | |
| } | |
| if !readUint24LengthPrefixedBytes(&data, &hint) || len(hint) == 0 { | |
| return nil, fmt.Errorf("reading hint for %s", tracePath) | |
| } | |
| if !data.Empty() { | |
| return nil, fmt.Errorf("trailing data found in %s", tracePath) | |
| } | |
| return &connectionTrace{ | |
| ClientHello: clientHello, | |
| HandshakeHint: hint, | |
| }, nil | |
| } | |
| // record updates the hint trace entry for a -Hints test. | |
| func (r *hintTraceRecorder) record(testBaseName string, configFlags []string, connections []connectionTrace) { | |
| trace := &hintTrace{ | |
| Connections: connections, | |
| ConfigFlags: configFlags, | |
| Date: time.Now().Format("20060102"), // YYYYMMDD | |
| } | |
| r.mu.Lock() | |
| defer r.mu.Unlock() | |
| r.traces[testBaseName] = trace | |
| } | |
| // numTraces returns the number of traces collected so far. | |
| func (r *hintTraceRecorder) numTraces() int { | |
| r.mu.Lock() | |
| defer r.mu.Unlock() | |
| return len(r.traces) | |
| } | |
| // writeTraces writes all collected hint traces to a JSON file in the trace | |
| // directory. | |
| func (r *hintTraceRecorder) writeTraces() error { | |
| if err := os.MkdirAll(r.dir, 0755); err != nil { | |
| return err | |
| } | |
| r.mu.Lock() | |
| defer r.mu.Unlock() | |
| var buf bytes.Buffer | |
| enc := json.NewEncoder(&buf) | |
| enc.SetEscapeHTML(false) | |
| enc.SetIndent("", " ") | |
| if err := enc.Encode(r.traces); err != nil { | |
| return err | |
| } | |
| return os.WriteFile(filepath.Join(r.dir, hintTracesFile), buf.Bytes(), 0644) | |
| } |