Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

125 lines
4.3 KiB
Go

// Package licenses recognises SPDX license headers in source files,
// producing one license node per distinct SPDX identifier and an
// EdgeLicensedAs from each file to the relevant license. Lets agents
// answer "what's the license of this file" and "find every file
// shipping under GPL" without grepping.
//
// Scope (v1): per-file SPDX-License-Identifier headers in the first
// ten lines of source. The repo-level LICENSE-file fallback is not
// emitted from here — it would belong at MultiIndexer level (one
// pass per repo on warmup) and adds setup state out of scope here.
// Files without a header simply produce no license edge; agents can
// detect that gap with a graph query.
package licenses
import (
"bufio"
"bytes"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/zzet/gortex/internal/graph"
)
// scanBufPool holds reusable 64 KB scratch buffers for bufio.Scanner.
// See internal/codegen/scanner.go for the same rationale — license
// header detection runs on every indexed file too.
var scanBufPool = sync.Pool{
New: func() any {
b := make([]byte, 64*1024)
return &b
},
}
// spdxRe matches the canonical SPDX header line. The standard form
// is:
//
// SPDX-License-Identifier: <expression>
//
// Authors usually wrap it in a comment opener. We accept any
// whitespace + comment-opener prefix on the same line, mirroring the
// TODO scanner's "comment context" rule. The captured group is the
// SPDX expression — we keep it verbatim (including AND / OR / WITH)
// so license-expression queries still see the structure.
var spdxRe = regexp.MustCompile(`(?:^|[\s/#*\-]+)SPDX-License-Identifier:\s*([^\n\r*/]+?)\s*$`)
// maxScanLines is the per-file scan window. SPDX guidance puts the
// identifier on the first line after the optional shebang; ten is a
// generous upper bound that accommodates copyright headers and
// "Code generated …" comments without scanning whole files.
const maxScanLines = 10
// Scan returns the SPDX identifier found in the file header, or ""
// if none was present. The returned string is the verbatim
// expression (no normalisation) — callers that need a canonical
// form should normalise downstream.
func Scan(source []byte) string {
if len(source) == 0 {
return ""
}
bufPtr := scanBufPool.Get().(*[]byte)
defer scanBufPool.Put(bufPtr)
scanner := bufio.NewScanner(bytes.NewReader(source))
scanner.Buffer(*bufPtr, 1024*1024)
for i := 0; i < maxScanLines && scanner.Scan(); i++ {
line := scanner.Text()
if !strings.Contains(line, "SPDX-License-Identifier") {
continue
}
if m := spdxRe.FindStringSubmatch(line); m != nil {
return strings.TrimSpace(m[1])
}
}
return ""
}
// BuildGraphArtifacts converts a per-file SPDX result into the node
// and edge to append. The license node is shared across files in a
// repo: a single `license::<spdx>` node per distinct expression.
// graph.AddNode is idempotent on ID, so emitting the same license
// for every file is cheap.
//
// In multi-repo mode the indexer's applyRepoPrefix pass prepends the
// repo prefix to every node ID — this is the same behaviour
// `annotation::<lang>::<name>` nodes get today. So a "shared license
// node" is repo-scoped, not graph-wide. Cross-repo de-duplication is
// out of scope for v1.
//
// filePath is the unprefixed path; applyRepoPrefix handles multi-
// repo namespacing downstream.
func BuildGraphArtifacts(filePath, spdx, language string) ([]*graph.Node, []*graph.Edge) {
if spdx == "" {
return nil, nil
}
filePath = filepath.ToSlash(filePath)
licenseID := LicenseNodeID(spdx)
licenseNode := &graph.Node{
ID: licenseID,
Kind: graph.KindLicense,
Name: spdx,
FilePath: filePath, // file path of first sighting; not authoritative
Language: language,
Meta: map[string]any{
"spdx": spdx,
},
}
edge := &graph.Edge{
From: filePath,
To: licenseID,
Kind: graph.EdgeLicensedAs,
FilePath: filePath,
Origin: graph.OriginASTResolved,
}
return []*graph.Node{licenseNode}, []*graph.Edge{edge}
}
// LicenseNodeID returns the canonical ID for a license node. Shared
// across files within a repo (one node per distinct SPDX
// expression). The "license::" prefix follows the same synthetic-
// node convention as `annotation::<lang>::<name>`.
func LicenseNodeID(spdx string) string {
return "license::" + spdx
}