syft/syft/pkg/cataloger/ocaml/parse_opam.go
Laurent Goderre 9c2799e379
Add the Ocaml ecosystem (#3112)
Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
2024-09-10 10:35:18 -04:00

151 lines
3.4 KiB
Go

package ocaml
import (
"context"
"encoding/json"
"io"
"path"
"regexp"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
//nolint:funlen
func parseOpamPackage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
opamVersionRe := regexp.MustCompile(`(?m)opam-version:\s*"[0-9]+\.[0-9]+"`)
versionRe := regexp.MustCompile(`(?m)^version:\s*"(?P<version>[^"]*)"`)
licenseRe := regexp.MustCompile(`(?m)^license:\s*(?P<license>(?:"[^"]*")|(?:\[[^\]]*\]))`)
homepageRe := regexp.MustCompile(`(?m)homepage:\s*"(?P<url>[^"]+)"`)
urlRe := regexp.MustCompile(`(?m)url\s*{(?P<url>[^}]+)}`)
data, err := io.ReadAll(reader)
if err != nil {
log.WithFields("error", err).Trace("unable to read opam package")
return nil, nil, nil
}
if opamVersionRe.FindSubmatch(data) == nil {
log.WithFields("warning", err).Trace("opam version not found")
return nil, nil, nil
}
// If name is inferred from file name/path
var name, version string
var licenses []string
loc := reader.Location.LocationData.AccessPath
dir, file := path.Split(loc)
if file == "opam" {
// folder name is the package name and version
s := strings.SplitN(path.Base(dir), ".", 2)
name = s[0]
if len(s) > 1 {
version = s[1]
}
} else {
// filename is the package name and version is in the content
name = strings.Replace(file, ".opam", "", 1)
m := versionRe.FindSubmatch(data)
if m != nil {
version = string(m[1])
}
}
entry := pkg.OpamPackage{
Name: name,
Version: version,
}
licenseMatch := licenseRe.FindSubmatch(data)
if licenseMatch != nil {
licenses = parseLicenses(string(licenseMatch[1]))
entry.Licenses = licenses
}
urlMatch := urlRe.FindSubmatch(data)
if urlMatch != nil {
url, checksums := parseURL(urlMatch[1])
if url != "" {
entry.URL = url
}
if checksums != nil {
entry.Checksums = checksums
}
}
homepageMatch := homepageRe.FindSubmatch(data)
if homepageMatch != nil {
entry.Homepage = string(homepageMatch[1])
}
pkgs = append(
pkgs,
newOpamPackage(
entry,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
)
return pkgs, nil, nil
}
func parseLicenses(licensesStr string) []string {
licenses := []string{}
if licensesStr[:1] == `"` {
content := licensesStr[1 : len(licensesStr)-1]
licenses = append(licenses, content)
} else {
var d []string
err := json.Unmarshal([]byte(licensesStr), &d)
if err == nil {
licenses = append(licenses, d...)
}
}
return licenses
}
func parseURL(data []byte) (string, []string) {
urlRe := regexp.MustCompile(`(?m)src:\s*"(?P<url>.*)"`)
checksumsRe := regexp.MustCompile(`(?m)checksum:\s*("[^"]*"|\[\s*((?:"[^"]*"\s*)+)\])`)
urlMatch := urlRe.FindSubmatch(data)
if urlMatch == nil {
return "", nil
}
url := urlMatch[1]
var checksum []string
checksumMatch := checksumsRe.FindSubmatch(data)
if checksumMatch != nil {
var fields []string
if checksumMatch[2] != nil {
fields = strings.Fields(string(checksumMatch[2]))
} else {
fields = strings.Fields(string(checksumMatch[1]))
}
for _, f := range fields {
checksum = append(checksum, strings.ReplaceAll(f, `"`, ""))
}
}
return string(url), checksum
}