blob: fb5b2fa1e17e969962ab4e1e913d983ad34f82fe [file] [edit]
// 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)
}