Compare commits

..

1 Commits

13 changed files with 108 additions and 135 deletions

4
.gitmodules vendored
View File

@ -70,7 +70,3 @@
path = examples/cpplint/simple
url = ssh://git@focs.ji.sjtu.edu.cn:2222/JOJ/JOJ3-examples.git
branch = cpplint/simple
[submodule "examples/healthcheck/whitelistedchars-success"]
path = examples/healthcheck/whitelistedchars-success
url = ssh://git@focs.ji.sjtu.edu.cn:2222/JOJ/JOJ3-examples.git
branch = healthcheck/whitelistedchars-success

View File

@ -22,7 +22,7 @@ $ git clone ssh://git@focs.ji.sjtu.edu.cn:2222/JOJ/JOJ3.git
2. Install [Go](https://go.dev/doc/install). Also, make sure `make` and `git` are installed and all 3 programs are presented in `$PATH`.
- If you have problem on connecting to the Go website and Go packages, download Go from [studygolang](https://studygolang.com/dl) and run `go env -w GOPROXY=https://goproxy.cn,direct` to set the Go modules mirror proxy after installing Go.
- If you have problem on connecting to the Go website and Go packages, download Go from [studygolang](https://studygolang.com/dl) and run `go env -w GOPROXY=https://goproxy.io,direct` to set the Go modules mirror proxy after installing Go.
3. Enable cgroup v2 for your OS. For WSL2, check [here](https://stackoverflow.com/a/73376219/13724598). Also, enable linger for the user you used to run `go-judge` if you are using `systemd`, e.g. if the user is `go-judge`, run `loginctl enable-linger go-judge`. So that you do not need root permission to run `go-judge` (it can create a nesting cgroup in its user slice).

View File

@ -187,12 +187,14 @@ func GetConfPath(confRoot, confName, fallbackConfName, msg, tag string) (
// Check file ownership
if stat, ok := confStat.Sys().(*syscall.Stat_t); ok {
uid := int(stat.Uid)
currentUID := os.Getuid()
if uid != currentUID {
err = fmt.Errorf("insecure configuration file: owned by uid %d, expected %d", uid, currentUID)
slog.Error("insecure conf file", "path", confPath, "uid", uid, "currentUID", currentUID)
currentUid := os.Getuid()
if uid != 0 && uid != currentUid {
err = fmt.Errorf("insecure configuration file: owned by uid %d, expected 0 or %d", uid, currentUid)
slog.Error("insecure conf file", "path", confPath, "uid", uid, "expected_uid", currentUid)
return confPath, confStat, conventionalCommit, err
}
} else {
slog.Warn("could not determine file ownership, proceeding with caution", "path", confPath)
}
return confPath, confStat, conventionalCommit, err
@ -202,12 +204,32 @@ func MatchGroups(conf *Conf, conventionalCommit *ConventionalCommit) []string {
seen := make(map[string]bool)
keywords := []string{}
loweredCommitGroup := strings.ToLower(conventionalCommit.Group)
matchAllGroups := loweredCommitGroup == "all"
if loweredCommitGroup == "all" {
for i := range conf.PreStages {
conf.PreStages[i].Group = ""
conf.PreStages[i].Groups = nil
}
for i := range conf.Stages {
conf.Stages[i].Group = ""
conf.Stages[i].Groups = nil
}
for i := range conf.PostStages {
conf.PostStages[i].Group = ""
conf.PostStages[i].Groups = nil
}
}
confStages := []ConfStage{}
confStages = append(confStages, conf.PreStages...)
confStages = append(confStages, conf.Stages...)
confStages = append(confStages, conf.PostStages...)
for _, stage := range confStages {
if stage.Group != "" {
keyword := strings.ToLower(stage.Group)
if _, exists := seen[keyword]; !exists {
seen[keyword] = true
keywords = append(keywords, keyword)
}
}
if len(stage.Groups) > 0 {
for _, group := range stage.Groups {
keyword := strings.ToLower(group)
@ -221,21 +243,10 @@ func MatchGroups(conf *Conf, conventionalCommit *ConventionalCommit) []string {
slog.Info("group keywords from stages", "keywords", keywords)
groups := []string{}
for _, keyword := range keywords {
if matchAllGroups || strings.Contains(loweredCommitGroup, keyword) {
if strings.Contains(loweredCommitGroup, keyword) {
groups = append(groups, keyword)
}
}
slog.Info("matched groups", "groups", groups)
if matchAllGroups {
for i := range conf.PreStages {
conf.PreStages[i].Groups = nil
}
for i := range conf.Stages {
conf.Stages[i].Groups = nil
}
for i := range conf.PostStages {
conf.PostStages[i].Groups = nil
}
}
return groups
}

View File

@ -6,6 +6,7 @@ import (
type ConfStage struct {
Name string
Group string // TODO: remove Group in the future
Groups []string
Executor struct {
Name string
@ -30,6 +31,15 @@ type Conf struct {
PreStages []ConfStage
Stages []ConfStage
PostStages []ConfStage
// TODO: remove this nested struct
Stage struct {
SandboxExecServer string
SandboxToken string
OutputPath string
PreStages []ConfStage
Stages []ConfStage
PostStages []ConfStage
}
}
type OptionalCmd struct {

View File

@ -78,6 +78,31 @@ func loadConf(confPath string) (*joj3Conf.Conf, error) {
slog.Error("parse conf", "error", err)
return nil, err
}
// TODO: remove this compatible code for nested struct
if conf.Stage.SandboxExecServer != "" {
conf.SandboxExecServer = conf.Stage.SandboxExecServer
conf.Stage.SandboxExecServer = ""
}
if conf.Stage.SandboxToken != "" {
conf.SandboxToken = conf.Stage.SandboxToken
conf.Stage.SandboxToken = ""
}
if conf.Stage.OutputPath != "" {
conf.OutputPath = conf.Stage.OutputPath
conf.Stage.OutputPath = ""
}
if len(conf.Stage.PreStages) > 0 {
conf.PreStages = conf.Stage.PreStages
conf.Stage.PreStages = nil
}
if len(conf.Stage.Stages) > 0 {
conf.Stages = conf.Stage.Stages
conf.Stage.Stages = nil
}
if len(conf.Stage.PostStages) > 0 {
conf.PostStages = conf.Stage.PostStages
conf.Stage.PostStages = nil
}
slog.Debug("conf loaded", "conf", conf, "joj3 version", Version)
return conf, nil
}

View File

@ -44,13 +44,21 @@ func generateStages(confStages []conf.ConfStage, groups []string) (
stages := []stage.Stage{}
existNames := map[string]bool{}
for i, s := range confStages {
if len(groups) == 0 && (len(s.Groups) != 0) {
if len(groups) == 0 && (len(s.Groups) != 0 || s.Group != "") {
continue
}
ok := false
if len(s.Groups) == 0 {
if s.Group == "" && len(s.Groups) == 0 {
ok = true
}
if !ok && s.Group != "" {
for _, group := range groups {
if strings.EqualFold(group, s.Group) {
ok = true
break
}
}
}
if !ok && len(s.Groups) > 0 {
for _, group := range groups {
for _, g := range s.Groups {

View File

@ -45,7 +45,6 @@ var (
checkFileNameList string
checkFileSumList string
metaFile []string
whitelistedChars string
allowedDomainList string
actorCsvPath string
showVersion *bool
@ -58,7 +57,6 @@ func init() {
flag.Float64Var(&repoSize, "repoSize", 2, "maximum size of the repo in MiB")
flag.StringVar(&checkFileNameList, "checkFileNameList", "", "comma-separated list of files to check")
flag.StringVar(&checkFileSumList, "checkFileSumList", "", "comma-separated list of expected checksums")
flag.StringVar(&whitelistedChars, "whitelistedChars", "", "comma-separated list of non-ASCII characters allowed in files")
flag.StringVar(&allowedDomainList, "allowedDomainList", "sjtu.edu.cn", "comma-separated list of allowed domains for commit author email")
flag.StringVar(&actorCsvPath, "actorCsvPath", "/home/tt/.config/joj/students.csv", "path to actor csv file")
parseMultiValueFlag(&metaFile, "meta", "meta files to check")
@ -76,14 +74,12 @@ func main() {
"repoSize", repoSize,
"checkFileNameList", checkFileNameList,
"checkFileSumList", checkFileSumList,
"whitelistedChars", whitelistedChars,
"meta", metaFile,
)
res := healthcheck.All(
rootDir,
checkFileNameList,
checkFileSumList,
whitelistedChars,
allowedDomainList,
actorCsvPath,
metaFile,

@ -1 +0,0 @@
Subproject commit 08aaaf1c3ab5863cc53475d8cbca0d668d6a8a1d

2
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7
github.com/mcuadros/go-defaults v1.2.0
github.com/mitchellh/mapstructure v1.5.0
google.golang.org/grpc v1.79.3
google.golang.org/grpc v1.79.1
google.golang.org/protobuf v1.36.11
)

25
go.sum
View File

@ -11,8 +11,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/criyle/go-judge/pb v1.3.2 h1:S0c0EqRF+xePOwcZxSb9mPV+bkXgfOX9f7SQMrcdeb4=
@ -40,6 +38,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@ -102,16 +102,21 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
@ -139,8 +144,10 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -3,12 +3,18 @@
// used for passing run time parameters.
package local
import "github.com/joint-online-judge/JOJ3/internal/stage"
import (
"os"
"github.com/joint-online-judge/JOJ3/internal/stage"
)
var name = "local"
type Local struct{}
func init() {
stage.RegisterExecutor(name, &Local{})
if os.Getenv("JOJ3_ENABLE_LOCAL_EXECUTOR") == "true" {
stage.RegisterExecutor(name, &Local{})
}
}

View File

@ -12,8 +12,7 @@ type Result struct {
}
func All(
rootDir, checkFileNameList, checkFileSumList, whitelistedChars,
allowedDomainList, actorCsvPath string,
rootDir, checkFileNameList, checkFileSumList, allowedDomainList, actorCsvPath string,
metaFile []string, repoSize float64,
) (res Result) {
var err error
@ -45,7 +44,7 @@ func All(
} else {
res.Msg += "### Meta File Check Passed\n"
}
err = NonASCIIFiles(rootDir, whitelistedChars)
err = NonASCIIFiles(rootDir)
if err != nil {
res.Msg += fmt.Sprintf("### Non-ASCII Characters File Check Failed:\n%s\n", err.Error())
res.Failed = true

View File

@ -8,83 +8,13 @@ import (
"path/filepath"
"strings"
"unicode"
"unicode/utf8"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/format/gitattributes"
)
// Read the list of comma-separated allowed characters from command line and convert it to a hashmap.
func parseWhitelistedChars(csv string) map[rune]struct{} {
whitelist := make(map[rune]struct{})
if strings.TrimSpace(csv) == "" {
return whitelist
}
for _, raw := range strings.Split(csv, ",") {
elem := strings.TrimSpace(raw)
if elem == "" {
slog.Warn("ignoring invalid whitelisted-chars element", "element", raw, "reason", "empty element")
continue
}
if utf8.RuneCountInString(elem) != 1 {
slog.Warn("ignoring invalid whitelisted-chars element", "element", elem, "reason", "element must be exactly one character")
continue
}
ch, _ := utf8.DecodeRuneInString(elem)
if ch == utf8.RuneError {
slog.Warn("ignoring invalid whitelisted-chars element", "element", elem, "reason", "invalid utf-8 rune")
continue
}
if ch <= unicode.MaxASCII {
slog.Warn("ignoring invalid whitelisted-chars element", "element", elem, "reason", "ASCII characters are not allowed")
continue
}
whitelist[ch] = struct{}{}
}
return whitelist
}
// getSubmodulePathsFromGoGit uses the go-git library to open the repository
// at the given root path and retrieve a list of all submodule paths.
// It returns a set of submodule paths for efficient lookup.
func getSubmodulePathsFromGoGit(root string) (map[string]struct{}, error) {
submodulePaths := make(map[string]struct{})
// Open the git repository at the given path.
repo, err := git.PlainOpen(root)
if err != nil {
if err == git.ErrRepositoryNotExists {
return submodulePaths, nil
}
return nil, fmt.Errorf("error opening git repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return nil, fmt.Errorf("error getting worktree: %w", err)
}
// Get the list of submodules.
submodules, err := worktree.Submodules()
if err != nil {
return nil, fmt.Errorf("error getting submodules: %w", err)
}
for _, sm := range submodules {
submodulePaths[filepath.ToSlash(sm.Config().Path)] = struct{}{}
}
return submodulePaths, nil
}
// getNonASCII retrieves a list of files in the specified root directory that contain non-ASCII characters.
// It searches for non-ASCII characters in each file's content and returns a list of paths to files containing non-ASCII characters.
func getNonASCII(root string, whitelist map[rune]struct{}) ([]string, error) {
func getNonASCII(root string) ([]string, error) {
var nonASCII []string
gitattrExist := true
var matcher gitattributes.Matcher
@ -93,11 +23,6 @@ func getNonASCII(root string, whitelist map[rune]struct{}) ([]string, error) {
gitattrExist = false
}
submodules, err := getSubmodulePathsFromGoGit(root)
if err != nil {
return nil, err
}
if gitattrExist {
fs := os.DirFS(".")
f, err := fs.Open(".gitattributes")
@ -117,22 +42,18 @@ func getNonASCII(root string, whitelist map[rune]struct{}) ([]string, error) {
return err
}
relPath, err := filepath.Rel(root, path)
if err != nil {
return err
}
if info.IsDir() {
if info.Name() == ".git" {
return filepath.SkipDir
}
if _, isSubmodule := submodules[relPath]; isSubmodule {
return filepath.SkipDir
}
return nil
}
if gitattrExist {
relPath, err := filepath.Rel(root, path)
if err != nil {
return err
}
ret, matched := matcher.Match(strings.Split(relPath, "/"), nil)
if matched && ret["text"].IsUnset() && !ret["text"].IsSet() {
return nil
@ -149,9 +70,6 @@ func getNonASCII(root string, whitelist map[rune]struct{}) ([]string, error) {
for scanner.Scan() {
cont := true
for _, c := range scanner.Text() {
if _, ok := whitelist[c]; ok {
continue
}
if c > unicode.MaxASCII {
nonASCII = append(nonASCII, "\t"+path)
cont = false
@ -171,10 +89,8 @@ func getNonASCII(root string, whitelist map[rune]struct{}) ([]string, error) {
// NonASCIIFiles checks for non-ASCII characters in files within the specified root directory.
// It prints a message with the paths to files containing non-ASCII characters, if any.
// Additionally it accept a list of whitelisted characters that are allowed, repo-wide.
func NonASCIIFiles(root, whitelistedChars string) error {
whitelist := parseWhitelistedChars(whitelistedChars)
nonASCII, err := getNonASCII(root, whitelist)
func NonASCIIFiles(root string) error {
nonASCII, err := getNonASCII(root)
if err != nil {
slog.Error("getting non-ascii", "err", err)
return fmt.Errorf("error getting non-ascii: %w", err)