Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions benches/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! the reference runner; here we watch for regressions in relative throughput.
//!
//! Run with `cargo bench`; results land under `target/criterion/`.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

use std::path::Path;

Expand Down Expand Up @@ -74,3 +75,4 @@ fn bench_scan(c: &mut Criterion) {

criterion_group!(benches, bench_scan);
criterion_main!(benches);
// REUSE-IgnoreEnd
4 changes: 3 additions & 1 deletion docs/REUSE_Specification_v3.3.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- REUSE-IgnoreStart β€” SPDX tags in this spec are illustrative examples, not licensing of this file. -->
# REUSE Specification – Version 3.3

> 2024-11-14
Expand Down Expand Up @@ -269,4 +270,5 @@ SPDX-SnippetCopyrightText: (C) Example Cooperative <info@coop.example.com>
Β© Example Corporation <https://corp.example.com>
Copyright 2016, 2018-2019 Joe Anybody
Copyright (c) Alice, some rights reserved
```
```
<!-- REUSE-IgnoreEnd -->
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ python = "3.13.14"
rust = "nightly"
git-cliff = "latest"
cargo-release = "latest"
'cargo:licet' = "0.1.4"
"cargo:licet" = "0.2.1"

[tools.'pipx:reuse']
version = "latest"
Expand Down
79 changes: 69 additions & 10 deletions src/cli/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,76 @@ pub fn run(args: InitArgs) -> Result<ExitCode> {
Ok(ExitCode::Success)
}

/// Serializable shape of the generated `license.toml`. Emitted via the `toml` crate so any
/// license id or extension is correctly quoted/escaped β€” hand-rolled `"{d}"` interpolation
/// previously produced invalid TOML for values containing quotes, backslashes, or newlines.
#[derive(serde::Serialize)]
struct GeneratedConfig {
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<DefaultSection>,
#[serde(rename = "rule", skip_serializing_if = "Vec::is_empty")]
rules: Vec<RuleSection>,
}

#[derive(serde::Serialize)]
struct DefaultSection {
license: String,
}

#[derive(serde::Serialize)]
struct RuleSection {
ext: String,
license: String,
}

fn render_config(default: Option<&str>, rules: &[(String, String)]) -> String {
let mut out = String::new();
out.push_str("# Generated by `licet init` from existing repository state.\n\n");
if let Some(d) = default {
out.push_str("[default]\n");
out.push_str(&format!("license = \"{d}\"\n\n"));
let config = GeneratedConfig {
default: default.map(|d| DefaultSection {
license: d.to_string(),
}),
rules: rules
.iter()
.map(|(ext, lic)| RuleSection {
ext: ext.clone(),
license: lic.clone(),
})
.collect(),
};
// This flat, string-only shape always serializes; fall back to an empty body rather
// than panicking if that ever changes.
let body = toml::to_string(&config).unwrap_or_default();
format!("# Generated by `licet init` from existing repository state.\n\n{body}")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn generated_toml_is_well_formed_for_adversarial_values() {
// Adversarial values (quotes, backslashes, newlines) must be escaped, not
// interpolated raw β€” the original `format!("license = \"{d}\"")` emitted invalid
// TOML so `init` crashed with a *TOML parse error* validating its own output.
// Escaping reduces that to, at worst, a clear semantic SPDX error from the loader.
let toml = render_config(
Some("MIT\nx=1\n\")"),
&[("r\"s".to_string(), "Apache-2.0 OR \"GPL\"".to_string())],
);
toml::from_str::<toml::Value>(&toml)
.expect("generated output must be syntactically valid TOML");
}
for (ext, lic) in rules {
out.push_str("[[rule]]\n");
out.push_str(&format!("ext = \"{ext}\"\n"));
out.push_str(&format!("license = \"{lic}\"\n\n"));

#[test]
fn render_is_well_formed_for_ordinary_input() {
let toml = render_config(Some("MIT"), &[("rs".to_string(), "Apache-2.0".to_string())]);
assert!(
toml.contains("[default]") && toml.contains("license = \"MIT\""),
"{toml}"
);
assert!(
toml.contains("[[rule]]") && toml.contains("ext = \"rs\""),
"{toml}"
);
LicensingConfiguration::from_toml(&toml).expect("ordinary config must parse");
}
out
}
31 changes: 28 additions & 3 deletions src/detect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::domain::{
Precedence,
};
use crate::reuse::oob::OutOfBand;
use crate::spdx;

/// Bytes of the file head scanned for headers. Headers live at the very top, so a few KB
/// is ample and keeps IO minimal.
Expand Down Expand Up @@ -306,12 +307,18 @@ fn parse_headers(text: &str) -> ParsedFile {
continue;
}

let lic = lic_at.map(|i| trim_value(&raw[i..]));
// Accept the value only when it is a well-formed SPDX expression. The tag is matched
// anywhere on the line (comment-syntax-agnostic), so without this guard any prose or
// code that merely follows the marker β€” e.g. the tag appearing inside a source
// string literal β€” would be captured verbatim as the file's license (FR-005).
let lic = lic_at
.map(|i| trim_value(&raw[i..]).trim().to_string())
.filter(|l| spdx::validate_expression(l).is_ok());

if in_snippet {
// Snippet licensing is gathered for inventory only β€” never file-level.
if let Some(l) = lic {
snippet_licenses.push(l.trim().to_string());
snippet_licenses.push(l);
}
continue;
}
Expand All @@ -330,7 +337,7 @@ fn parse_headers(text: &str) -> ParsedFile {
});
block.byte_range.1 = end;
if let Some(l) = lic {
block.license_ids.push(l.trim().to_string());
block.license_ids.push(l);
}
if let Some(c) = cpr {
block.copyrights.push(c.trim().to_string());
Expand Down Expand Up @@ -491,6 +498,24 @@ mod tests {
assert_eq!(h[0].license_ids, vec!["CC0-1.0".to_string()]);
}

#[test]
fn non_spdx_tag_value_is_not_captured_as_license() {
// The tag is matched anywhere on a line, so a tag appearing inside a source-string
// literal (e.g. this project's own test fixtures) trails non-SPDX junk after the id.
// Such a value must not be accepted as the file's license (would otherwise poison
// `init`'s generated config β€” regression for the unescaped-TOML crash).
let parsed =
parse_headers(" f.write(\"a.py\", \"# SPDX-License-Identifier: MIT\\nx=1\\n\")\n");
assert!(
parsed.blocks.iter().all(|b| b.license_ids.is_empty()),
"malformed SPDX expression must not be detected: {:?}",
parsed.blocks
);
// A clean id on its own line is still detected.
let ok = parse_headers("# SPDX-License-Identifier: MIT\n").blocks;
assert_eq!(ok[0].license_ids, vec!["MIT".to_string()]);
}

#[test]
fn block_closers_across_styles_are_trimmed() {
// One representative per registry block style; the trailing closer must be stripped
Expand Down
2 changes: 2 additions & 0 deletions src/reconcile/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub fn insert_header(content: &str, header: &str) -> String {
out
}

// REUSE-IgnoreStart β€” SPDX tags in the tests below are fixtures, not this file's licensing.
#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -137,3 +138,4 @@ mod tests {
);
}
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions src/reconcile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ fn extract_terminator(value: &str) -> String {
String::new()
}

// REUSE-IgnoreStart β€” SPDX tags in the tests below are fixtures, not this file's licensing.
#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -288,3 +289,4 @@ mod tests {
assert!(plan.new_content.is_none());
}
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions src/reuse/oob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ fn toml_string(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}

// REUSE-IgnoreStart β€” SPDX tags in the tests below are fixtures, not this file's licensing.
#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -518,3 +519,4 @@ mod tests {
);
}
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/determinism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! 3. The binary makes no network calls; `--allow-network` is advisory only. (The absence
//! of any network-capable crate in the dependency tree is the structural guarantee β€”
//! asserted in CI by `cargo tree`; here we confirm the offline paths behave identically.)
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -92,3 +93,4 @@ fn allow_network_flag_does_not_change_offline_behavior() {
"JSON lint posture must be identical regardless of --allow-network (no fetch occurs)"
);
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! well within budget. This is a coarse timing guard (not a microbenchmark) sized to be
//! stable in CI; the spec's 10k-file <1s warm / <3s cold bars are validated on the
//! reference 4-core runner. Here we assert a generous ceiling to catch gross regressions.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -33,3 +34,4 @@ fn scans_a_few_thousand_files_quickly() {
);
eprintln!("cold scan of {n} files: {cold:?}");
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/safety.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Apply-safety, config-error, and detection-precedence suites
//! (FR-024, SC-010, FR-003a, config validation β†’ exit 2).
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -129,3 +130,4 @@ fn line_endings_preserved_on_write() {
"CRLF preserved: {content:?}"
);
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us1_check_drift.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! US1 β€” declarative drift detection (SC-001, SC-003, FR-004). Mirrors quickstart Scenario 1.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -133,3 +134,4 @@ fn semantic_expression_equivalence_is_compliant() {
String::from_utf8_lossy(&out.stdout)
);
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us1_snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! These pin the exact rendered text of `licet check` so layout regressions are caught.
//! File ordering is deterministic (walk sorts by relative path), and all paths are
//! repo-relative, so the snapshots are stable across machines/tempdirs.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -54,3 +55,4 @@ fn compliant_report_human() {
let stdout = String::from_utf8(out.stdout).unwrap();
insta::assert_snapshot!(stdout);
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us2_apply.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! US2 β€” reconcile to intent (SC-002, SC-004, FR-006..FR-009, FR-020). Quickstart Scenario 2.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -120,3 +121,4 @@ fn target_header_replaces_chosen_block() {
);
assert!(!content.contains("ISC"), "second block replaced: {content}");
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us2_partial.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! US2 β€” partial apply (FR-021): when some writes succeed and others fail, exit 3 and
//! report changed vs unchanged files.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

#![cfg(unix)]

Expand Down Expand Up @@ -46,3 +47,4 @@ fn partial_apply_exits_3_and_reports_changed_vs_unchanged() {
);
assert!(f.read("ok/a.rs").contains("SPDX-License-Identifier: MIT"));
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us3_comment_style.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! US3 β€” new comment style persists & round-trips (SC-005, FR-010, FR-011).
//! Quickstart Scenario 3.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -66,3 +67,4 @@ fn inline_custom_style_is_used() {
"{content}"
);
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us4_enforce.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! US4 β€” enforce in hook / CI (SC-006, SC-009, FR-013, FR-025, FR-027).
//! Quickstart Scenario 4.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -84,3 +85,4 @@ fn explain_names_winning_rule() {
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("glob=examples/**/*.rs"), "{stdout}");
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us5_add_license.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! US5 β€” `add-license` offline text materialization (FR-017, FR-029).
//! The offline analog of `reuse download`: copy referenced texts into `LICENSES/` from the
//! embedded bundle, without touching source files or the config. Quickstart Scenario 5.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -149,3 +150,4 @@ fn json_output_is_well_formed() {
assert_eq!(v["summary"]["pass"], true);
assert!(v["license_texts"]["spdx_list_version"].is_string());
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us5_reuse.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! US5 β€” REUSE compatibility & migration (SC-007, SC-008, FR-014, FR-017, FR-018, FR-028).
//! Quickstart Scenario 5.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;
use common::Fixture;
Expand Down Expand Up @@ -100,3 +101,4 @@ fn which_reuse() -> Option<()> {
.filter(|o| o.status.success())
.map(|_| ())
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us5_sidecars.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `.license` sidecars and REUSE.toml coverage for non-annotatable files (FR-015),
//! plus REUSE 3.3 precedence on detection (FR-003a).
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;

Expand Down Expand Up @@ -238,3 +239,4 @@ fn reuse_toml_write_is_idempotent() {
assert_eq!(first, second, "REUSE.toml entry duplicated on re-apply");
assert_eq!(first.matches("path = \"logo.png\"").count(), 1);
}
// REUSE-IgnoreEnd
2 changes: 2 additions & 0 deletions tests/us5_snippets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! describes the snippet, not the file, so it never satisfies (or violates) the file's
//! declared intent β€” but its text still counts for `LICENSES/` completeness. Information
//! inside `REUSE-IgnoreStart`..`REUSE-IgnoreEnd` is dropped entirely.
// REUSE-IgnoreStart β€” SPDX tags below are test fixtures, not this file's licensing.

mod common;

Expand Down Expand Up @@ -100,3 +101,4 @@ fn ignore_block_hides_spdx_tags() {
1
);
}
// REUSE-IgnoreEnd
Loading