blob: b1421201f1e60d23db749ed85661951bdec8bf5e [file]
// Copyright 2026 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package main
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"sync/atomic"
"dawn.googlesource.com/dawn/tools/src/glob"
"dawn.googlesource.com/dawn/tools/src/oswrapper"
"dawn.googlesource.com/dawn/tools/src/progressbar"
"dawn.googlesource.com/dawn/tools/src/transform"
"dawn.googlesource.com/dawn/tools/src/utils"
"golang.org/x/sync/errgroup"
)
// runCorpusGenerator converts a set of input test files into a fuzzer corpus
// The generator will use t.inputs as the source directory.
// The corpus will be written to t.out.
func runCorpusGenerator(t *taskConfig) error {
switch t.fuzzMode {
case FuzzModeWgsl:
return runCorpusGeneratorWgsl(t)
case FuzzModeIr:
return runCorpusGeneratorIr(t)
default:
return fmt.Errorf("unknown fuzzer mode %d", t.fuzzMode)
}
}
// runCorpusGeneratorWgsl converts a set of input test .wgsl files into a WGSL fuzzer corpus.
func runCorpusGeneratorWgsl(t *taskConfig) error {
return gatherWgslFiles(t.inputs, t.out, t.osWrapper)
}
// runCorpusGeneratorIr converts a set of input test .wgsl files into an IR fuzzer corpus.
// It gathers the WGSL files, then forks out to external binary batches (t.assembler) in parallel
// across t.numProcesses workers to perform the conversion.
func runCorpusGeneratorIr(t *taskConfig) error {
tmp, err := t.osWrapper.MkdirTemp("", "wgsl_corpus_for_ir")
if err != nil {
return fmt.Errorf("failed to create temporary directory for WGSL files: %w", err)
}
defer t.osWrapper.RemoveAll(tmp)
files, err := gatherWgslFilesList(t.inputs, tmp, t.osWrapper)
if err != nil {
return fmt.Errorf("failed to gather WGSL files for IR corpus generation: %w", err)
}
if err := t.osWrapper.MkdirAll(t.out, 0755); err != nil {
return fmt.Errorf("failed to create output directory '%v': %w", t.out, err)
}
if len(files) == 0 {
fmt.Println("done")
return nil
}
numProcesses := max(1, t.numProcesses)
batchSize := calcBatchSize(len(files), numProcesses)
batches := batchFiles(files, batchSize)
batchChan := make(chan []string, len(batches))
for _, b := range batches {
batchChan <- b
}
close(batchChan)
fmt.Printf("running assembler across %d processes (%d files in %d batches)\n", numProcesses, len(files), len(batches))
var pb progressbar.ProgressBar
if t.progressBuilder != nil {
pb = t.progressBuilder(os.Stdout, nil)
defer pb.Stop()
}
var numDone atomic.Uint32
eg, ctx := errgroup.WithContext(utils.CancelOnInterruptContext(context.Background()))
for i := 0; i < numProcesses; i++ {
eg.Go(func() error {
for batch := range batchChan {
if err := ctx.Err(); err != nil {
return err
}
args := make([]string, 0, len(batch)+2)
if t.verbose {
args = append(args, "--verbose")
}
args = append(args, batch...)
args = append(args, t.out)
cmdStr := fmt.Sprintf("%s %s", t.assembler, strings.Join(args, " "))
if t.verbose {
fmt.Println("Using assembler cmd: " + cmdStr)
}
out := &bytes.Buffer{}
var stdout, stderr io.Writer = out, out
if t.verbose {
stdout = io.MultiWriter(out, os.Stdout)
stderr = io.MultiWriter(out, os.Stderr)
}
cmd := t.execWrapper.CommandContext(ctx, t.assembler, args...).WithStdout(stdout).WithStderr(stderr)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run IR corpus assembler.\n command: %s\n error: %w\n output:\n%s", cmdStr, err, out.String())
}
done := int(numDone.Add(uint32(len(batch))))
if pb != nil {
pb.Update(progressbar.Status{
Total: len(files),
Segments: []progressbar.Segment{
{Count: done},
},
})
}
}
return nil
})
}
if err := eg.Wait(); err != nil {
return err
}
if pb != nil {
pb.Stop()
}
fmt.Println("done")
return nil
}
var reCopyrightNotice = regexp.MustCompile(`(?s)^// Copyright.*?\r?\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE\.\r?\n\s*`)
// gatherWgslFiles cleans up and copies all the .wgsl files in a directory structure over to a flat directory
// structure, via replacing the path separators for the origins with underscores in the destination
// file names. It also filters out any '*.expected.*' files. If there are copyright notice blocks in the file contents
// they will be stripped, because they are known to cause issues when decoding fuzzer sidecar data.
func gatherWgslFiles(inputs string, out string, fsReaderWriter oswrapper.FilesystemReaderWriter) error {
_, err := gatherWgslFilesList(inputs, out, fsReaderWriter)
return err
}
func gatherWgslFilesList(inputs string, out string, fsReaderWriter oswrapper.FilesystemReaderWriter) ([]string, error) {
fmt.Println("gathering and filtering .wgsl files")
globPattern := filepath.Join(inputs, "**.wgsl")
files, err := glob.Glob(globPattern, fsReaderWriter)
if err != nil {
return nil, fmt.Errorf("failed to find .wgsl files with pattern '%v': %w", globPattern, err)
}
// Remove '*.expected.*'
files = transform.Filter(files, func(s string) bool { return !strings.Contains(s, ".expected.") })
if err := fsReaderWriter.MkdirAll(out, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory '%v': %w", out, err)
}
// Map src file paths to dst filenames where the path separators have been converted to underscores
mapping := make(map[string]string, len(files))
for _, f := range files {
// paths returned by glob.Glob are absolute, but only want to use the relative path in the dest name
relPath, err := filepath.Rel(inputs, f)
if err != nil {
return nil, fmt.Errorf("failed to calculate relative path for '%v' from base '%v': %w", f, inputs, err)
}
mapping[f] = strings.ReplaceAll(filepath.ToSlash(relPath), "/", "_")
}
outFiles := make([]string, 0, len(mapping))
for src, dest := range mapping {
dstPath := filepath.Join(out, dest)
contentBytes, err := fsReaderWriter.ReadFile(src)
if err != nil {
return nil, fmt.Errorf("failed to read '%v': %w", src, err)
}
contentBytes = reCopyrightNotice.ReplaceAll(contentBytes, nil)
if err := fsReaderWriter.WriteFile(dstPath, contentBytes, 0644); err != nil {
return nil, fmt.Errorf("failed to write '%v': %w", dstPath, err)
}
outFiles = append(outFiles, dstPath)
}
slices.Sort(outFiles)
fmt.Println("done")
return outFiles, nil
}