blob: 80532e494da8dc8bee58316a34dde1c9dc315839 [file] [log] [blame]
// Copyright 2023 The Shac 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
//
// http://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 engine
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"unsafe"
"go.starlark.net/starlark"
)
func ctxEmitFinding(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) error {
var arglevel starlark.String
var argmessage starlark.String
var argfilepath starlark.String
var argline starlark.Int
var argcol starlark.Int
var argendCol starlark.Int
var argendLine starlark.Int
var argreplacements starlark.Sequence
if err := starlark.UnpackArgs(name, args, kwargs,
"level", &arglevel,
"message?", &argmessage,
"filepath?", &argfilepath,
// The following arguments all use ?? so that None is interpreted the
// same as the argument being unset. This makes it easier to implement
// checks that run tools that emit JSON output that may contain null
// fields.
"line??", &argline,
"col??", &argcol,
"end_line??", &argendLine,
"end_col??", &argendCol,
"replacements??", &argreplacements,
); err != nil {
return err
}
level := Level(string(arglevel))
if !level.isValid() {
return fmt.Errorf("for parameter \"level\": got %s, want one of %q, %q or %q", arglevel, Notice, Warning, Error)
}
file := string(argfilepath)
span := Span{
Start: Cursor{
Line: intToInt(argline),
Col: intToInt(argcol),
},
End: Cursor{
Line: intToInt(argendLine),
Col: intToInt(argendCol),
},
}
if span.Start.Line <= -1 {
return fmt.Errorf("for parameter \"line\": got %s, line are 1 based", argline)
} else if span.Start.Col <= -1 {
return fmt.Errorf("for parameter \"col\": got %s, line are 1 based", argcol)
} else if span.End.Line <= -1 {
return fmt.Errorf("for parameter \"end_line\": got %s, line are 1 based", argendLine)
} else if span.End.Col <= -1 {
return fmt.Errorf("for parameter \"end_col\": got %s, line are 1 based", argendCol)
}
if span.Start.Col == 0 && span.End.Col > 0 {
return errors.New("for parameter \"end_col\": \"col\" must be specified")
}
if span.Start.Line > 0 {
if file == "" {
return errors.New("for parameter \"line\": \"filepath\" must be specified")
}
if span.End.Line > 0 {
if span.End.Line < span.Start.Line {
return fmt.Errorf("for parameter \"end_line\": \"end_line\" (%d) must be greater than or equal to \"line\" (%d)", span.End.Line, span.Start.Line)
} else if span.End.Line == span.Start.Line && span.End.Col > 0 && span.End.Col <= span.Start.Col {
return fmt.Errorf("for parameter \"end_col\": \"end_col\" (%d) must be greater than \"col\" (%d)", span.End.Col, span.Start.Col)
}
} else if span.End.Col > 0 {
// If end_col is set but end_line is unset, assume that end_line is
// equal to line.
span.End.Line = span.Start.Line
}
} else {
if span.End.Line > 0 {
return errors.New("for parameter \"end_line\": \"line\" must be specified")
}
if span.Start.Col > 0 {
return errors.New("for parameter \"col\": \"line\" must be specified")
}
}
var replacements []string
if argreplacements != nil {
if file == "" {
return errors.New("for parameter \"replacements\": \"filepath\" must be specified")
}
if replacements = sequenceToStrings(argreplacements); replacements == nil {
return fmt.Errorf("for parameter \"replacements\": got %s, want sequence of str", argreplacements.Type())
}
if len(replacements) > 100 {
return fmt.Errorf("for parameter \"replacements\": excessive number (%d) of replacements", len(replacements))
}
}
c := ctxCheck(ctx)
message := string(argmessage)
if len(message) == 0 {
if c.formatter && file != "" && len(replacements) == 1 {
// If the check is a formatter, and the finding would be fixed by
// `shac fmt`, let users omit `message` as long as `file` is
// specified, since `message` will always look something like the
// following for formatters.
message = "File not formatted. Run `shac fmt` to fix."
} else {
return fmt.Errorf("for parameter \"message\": must not be empty")
}
}
if c.highestLevel == "" || level == Error || (level == Warning && c.highestLevel != Error) {
c.highestLevel = level
}
root := ""
if file != "" {
root = filepath.Join(s.root, s.subdir)
// The file must be tracked by scm.
f, err := s.scm.allFiles(ctx, false)
if err != nil {
return err
}
if _, found := sort.Find(len(f), func(i int) int { return strings.Compare(file, f[i].relpath()) }); !found {
return fmt.Errorf("for parameter \"filepath\": %s is not tracked", argfilepath)
}
}
if err := s.r.EmitFinding(ctx, c.name, level, message, root, file, span, replacements); err != nil {
return fmt.Errorf("failed to emit: %w", err)
}
return nil
}
func ctxEmitArtifact(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) error {
var argfilepath starlark.String
var argcontent starlark.Value = starlark.None
if err := starlark.UnpackArgs(name, args, kwargs,
"filepath", &argfilepath,
"content?", &argcontent,
); err != nil {
return err
}
f := string(argfilepath)
var content []byte
root := ""
switch v := argcontent.(type) {
case starlark.Bytes:
content = unsafeByteSlice(string(v))
case starlark.String:
content = unsafeByteSlice(string(v))
case starlark.NoneType:
root = filepath.Join(s.root, s.subdir)
dst, err := absPath(f, root)
if err != nil {
return fmt.Errorf("for parameter \"filepath\": %s %w", argfilepath, err)
}
// Make sure the file exist, but do not load it.
if info, err := os.Stat(dst); err != nil {
if errors.Is(err, fs.ErrNotExist) {
// Hide the underlying error for determinism.
return fmt.Errorf("for parameter \"filepath\": %q not found", f)
}
// Something other than a file not found error, return it as is.
return fmt.Errorf("for parameter \"filepath\": %w", err)
} else if info.IsDir() {
return fmt.Errorf("for parameter \"filepath\": %q is a directory", f)
}
default:
return fmt.Errorf("for parameter \"content\": got %s, want str or bytes", argcontent.Type())
}
c := ctxCheck(ctx)
if err := s.r.EmitArtifact(ctx, c.name, root, f, content); err != nil {
return fmt.Errorf("failed to emit: %w", err)
}
return nil
}
// sequenceToStrings converts a starlark sequence (list, tuple) into a list of strings.
func sequenceToStrings(s starlark.Sequence) []string {
out := make([]string, 0, s.Len())
iter := s.Iterate()
var x starlark.Value
for iter.Next(&x) {
s, ok := x.(starlark.String)
if !ok {
return nil
}
out = append(out, string(s))
}
return out
}
// intToInt returns -1 on failure.
func intToInt(i starlark.Int) int {
i64, ok := i.Int64()
const maxInt = int64(int(^uint(0) >> 1))
if !ok || i64 < 0 || i64 > maxInt {
return -1
}
return int(i64)
}
func unsafeByteSlice(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}