// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 clbbbb
///|
pub struct ThirdPartyPackage {
name : String
license : String
source : String
notice : Bool
} derive(Debug, Eq)
///|
pub fn third_party_package(
name : String,
license : String,
source : String,
notice : Bool,
) -> ThirdPartyPackage {
{ name, license: normalize(license), source, notice }
}
///|
pub fn parse_third_party_manifest(text : String) -> Array[ThirdPartyPackage] {
let rows : Array[ThirdPartyPackage] = []
let mut name = ""
let mut license = ""
let mut source = ""
let mut notice = false
for line in split_char(text, '\n') {
let clean = trim_ascii(line)
if clean == "" {
add_third_party_if_complete(rows, name, license, source, notice)
name = ""
license = ""
source = ""
notice = false
} else if !clean.has_prefix("#") {
match third_party_field(clean) {
Some((key, value)) =>
if key == "name" {
name = value
} else if key == "license" {
license = value
} else if key == "source" {
source = value
} else if key == "notice" {
notice = parse_bool(value)
} else {
()
}
None => ()
}
}
}
add_third_party_if_complete(rows, name, license, source, notice)
rows
}
///|
pub fn third_party_manifest_errors(text : String) -> Array[String] {
let mut errors : Array[String] = []
let mut name = ""
let mut license = ""
let mut source = ""
let mut record_line = 0
let mut seen : Array[String] = []
let lines = split_char(text, '\n')
for index = 0; index < lines.length(); index = index + 1 {
let clean = trim_ascii(lines[index])
if clean == "" {
errors = errors +
third_party_record_errors(record_line, name, license, source)
name = ""
license = ""
source = ""
record_line = 0
seen = []
} else if !clean.has_prefix("#") {
if record_line == 0 {
record_line = index + 1
}
match third_party_field(clean) {
Some((key, value)) =>
if !is_third_party_field(key) {
errors.push(
"line " + (index + 1).to_string() + ": unknown field " + key,
)
} else if contains_string(seen, key) {
errors.push(
"line " + (index + 1).to_string() + ": duplicate field " + key,
)
} else if key == "notice" && !is_bool_config_value(value) {
errors.push(
"line " +
(index + 1).to_string() +
": notice must be true or false",
)
} else {
seen.push(key)
if key == "name" {
name = value
} else if key == "license" {
license = value
} else if key == "source" {
source = value
} else {
()
}
}
None =>
errors.push(
"line " + (index + 1).to_string() + ": expected key = value",
)
}
}
}
errors + third_party_record_errors(record_line, name, license, source)
}
///|
pub fn is_third_party_field(key : String) -> Bool {
key == "name" || key == "license" || key == "source" || key == "notice"
}
///|
pub fn is_bool_config_value(value : String) -> Bool {
let lower = lower_ascii(value)
lower == "true" || lower == "false"
}
///|
pub fn third_party_record_errors(
line : Int,
name : String,
license : String,
source : String,
) -> Array[String] {
let errors : Array[String] = []
if line > 0 {
if name == "" {
errors.push(
"record starting at line " + line.to_string() + ": name is missing",
)
}
if license == "" {
errors.push(
"record starting at line " + line.to_string() + ": license is missing",
)
}
if source == "" {
errors.push(
"record starting at line " + line.to_string() + ": source is missing",
)
}
}
errors
}
///|
pub fn third_party_manifest_valid(text : String) -> Bool {
third_party_manifest_errors(text).is_empty()
}
///|
pub fn third_party_field(line : String) -> (String, String)? {
match line.find("=") {
Some(index) => {
let key = lower_ascii(
trim_ascii(line.unsafe_substring(start=0, end=index)),
)
let raw = trim_ascii(
line.unsafe_substring(start=index + 1, end=line.length()),
)
Some((key, trim_config_value(raw)))
}
None => None
}
}
///|
pub fn trim_config_value(value : String) -> String {
match quoted_prefix(value) {
Some(text) => text
None => trim_ascii(value)
}
}
///|
pub fn add_third_party_if_complete(
rows : Array[ThirdPartyPackage],
name : String,
license : String,
source : String,
notice : Bool,
) -> Unit {
if name != "" || license != "" || source != "" {
rows.push(third_party_package(name, license, source, notice))
}
}
///|
pub fn third_party_errors(
packages : Array[ThirdPartyPackage],
policy : Policy,
) -> Array[String] {
let rows : Array[String] = []
for item in packages {
for error in third_party_package_errors(item, policy) {
rows.push(item.name + ": " + error)
}
}
duplicate_third_party_errors(packages, rows)
rows
}
///|
pub fn third_party_package_errors(
item : ThirdPartyPackage,
policy : Policy,
) -> Array[String] {
let rows : Array[String] = []
if item.name == "" {
rows.push("name is missing")
}
if item.source == "" {
rows.push("source URL is missing")
}
if item.license == "" {
rows.push("license is missing")
} else {
for error in validation_errors(item.license) {
rows.push(error)
}
if is_valid(item.license) {
for error in policy_errors(item.license, policy) {
rows.push(error)
}
}
}
if third_party_needs_notice(item) && !item.notice {
rows.push("notice acknowledgement is missing")
}
rows
}
///|
pub fn duplicate_third_party_errors(
packages : Array[ThirdPartyPackage],
rows : Array[String],
) -> Unit {
for index = 0; index < packages.length(); index = index + 1 {
let item = packages[index]
for earlier = 0; earlier < index; earlier = earlier + 1 {
if lower_ascii(item.name) == lower_ascii(packages[earlier].name) {
rows.push(item.name + ": duplicate package entry")
break
}
}
}
}
///|
pub fn third_party_needs_notice(item : ThirdPartyPackage) -> Bool {
if item.license == "" || !is_valid(item.license) {
false
} else {
let expr = parse_or_panic(item.license)
for id in licenses(expr) {
if license_needs_notice(id) {
return true
}
}
false
}
}
///|
pub fn license_needs_notice(id : String) -> Bool {
let license = canonical_license(id)
license != "CC0-1.0" && license != "Unlicense"
}
///|
pub fn third_party_licenses(
packages : Array[ThirdPartyPackage],
) -> Array[String] {
let rows : Array[String] = []
for item in packages {
if item.license != "" {
rows.push(item.license)
}
}
unique_strings(rows)
}
///|
pub fn third_party_without_notice(
packages : Array[ThirdPartyPackage],
) -> Array[ThirdPartyPackage] {
let rows : Array[ThirdPartyPackage] = []
for item in packages {
if third_party_needs_notice(item) && !item.notice {
rows.push(item)
}
}
rows
}
///|
pub fn third_party_by_license(
packages : Array[ThirdPartyPackage],
license : String,
) -> Array[ThirdPartyPackage] {
let rows : Array[ThirdPartyPackage] = []
let target = normalize(license)
for item in packages {
if item.license == target {
rows.push(item)
}
}
rows
}
///|
pub fn third_party_names(packages : Array[ThirdPartyPackage]) -> Array[String] {
let rows : Array[String] = []
for item in packages {
rows.push(item.name)
}
rows
}
///|
pub fn third_party_policy_passes(
packages : Array[ThirdPartyPackage],
policy : Policy,
) -> Bool {
third_party_errors(packages, policy).is_empty()
}
///|
pub fn third_party_report(
packages : Array[ThirdPartyPackage],
policy : Policy,
) -> String {
let rows : Array[String] = [
"third-party packages: " + packages.length().to_string(),
"licenses: " + join_with(third_party_licenses(packages), ","),
"notice acknowledgements missing: " +
third_party_without_notice(packages).length().to_string(),
]
push_section(rows, "third-party errors", third_party_errors(packages, policy))
join_lines(rows)
}
///|
pub fn third_party_markdown(
packages : Array[ThirdPartyPackage],
policy : Policy,
) -> String {
let rows : Array[String] = [
"# Third-party license inventory", "", "| Package | License | Source | Notice |",
"| --- | --- | --- | --- |",
]
for item in packages {
rows.push(
"| " +
item.name +
" | " +
item.license +
" | " +
item.source +
" | " +
bool_word(item.notice) +
" |",
)
}
rows.push("")
push_markdown_section(
rows,
"Validation errors",
third_party_errors(packages, policy),
)
join_lines(rows)
}
///|
pub fn third_party_license_summary(
packages : Array[ThirdPartyPackage],
) -> String {
let rows : Array[String] = []
for license in third_party_licenses(packages) {
rows.push(
license +
": " +
third_party_by_license(packages, license).length().to_string(),
)
}
if rows.is_empty() {
"no third-party packages"
} else {
join_lines(rows)
}
}
///|
pub fn third_party_sources_missing(
packages : Array[ThirdPartyPackage],
) -> Array[String] {
let rows : Array[String] = []
for item in packages {
if item.source == "" {
rows.push(item.name)
}
}
rows
}
///|
pub fn third_party_count_for_license(
packages : Array[ThirdPartyPackage],
license : String,
) -> Int {
third_party_by_license(packages, license).length()
}
///|
pub fn third_party_invalid_licenses(
packages : Array[ThirdPartyPackage],
) -> Array[String] {
let rows : Array[String] = []
for item in packages {
if item.license == "" || !is_valid(item.license) {
rows.push(item.name)
}
}
rows
}
///|
pub fn third_party_risk_summary(packages : Array[ThirdPartyPackage]) -> String {
let low : Array[String] = []
let medium : Array[String] = []
let high : Array[String] = []
let unknown : Array[String] = []
for item in packages {
if item.license == "" || !is_valid(item.license) {
unknown.push(item.name)
} else {
match highest_risk(item.license) {
"low" => low.push(item.name)
"medium" => medium.push(item.name)
"high" => high.push(item.name)
_ => unknown.push(item.name)
}
}
}
join_lines([
"low: " + join_with(low, ","),
"medium: " + join_with(medium, ","),
"high: " + join_with(high, ","),
"unknown: " + join_with(unknown, ","),
])
}
///|
pub fn third_party_release_ready(
packages : Array[ThirdPartyPackage],
policy : Policy,
) -> Bool {
third_party_policy_passes(packages, policy) &&
third_party_sources_missing(packages).is_empty() &&
third_party_without_notice(packages).is_empty()
}
///|
pub fn third_party_release_report(
packages : Array[ThirdPartyPackage],
policy : Policy,
) -> String {
let rows : Array[String] = [
"third-party release ready: " +
bool_word(third_party_release_ready(packages, policy)),
"",
third_party_report(packages, policy),
"",
"risk summary",
third_party_risk_summary(packages),
]
let missing = third_party_sources_missing(packages)
if !missing.is_empty() {
rows.push("")
rows.push("packages without source URL: " + join_with(missing, ","))
}
join_lines(rows)
}