crates/nornir-build-thing/src/kernel_trim/coverage.rs
//! **The measured kernel trim — the COVERAGE half** (tunnr 1.1 decision
//! 2026-10-30, Heimdall tooling plan step 2).
//!
//! The static half ([`super::TrimModel`]) says what a row COULD drop. This
//! half says what the row's check suite never touched:
//!
//! 1. **The probe kernel** — `--opt kernel.coverage=gcov` lands
//! [`KernelProbe::Gcov`] on the row's kernel: [`GCOV_PROBE_FRAGMENT`]
//! (`GCOV_KERNEL` + `GCOV_PROFILE_ALL`, `DEBUG_FS` which gcov needs, and
//! KASLR off so the link addresses are the run addresses) merged after
//! every other layer. It is a DEV probe: a build of it for a release or a
//! signed medium is refused by name ([`refuse_probe`]).
//! 2. **The counters, read from outside** — the probe kernel boots under KVM
//! and runs the row's checks; at each stage the suite saves the kernel image
//! (`_text` .. `_end`) out of the VM with the QEMU monitor (`memsave`).
//! [`walk_gcov`] follows the kernel's own `gcov_info` list in that dump
//! (the layout of `kernel/gcov/gcc_4_7.c` for the compiler named in
//! `vmlinux`'s `.comment`) — nothing runs in the guest, no debugfs is
//! mounted, no evidence channel is needed.
//! 3. **Objects → `CONFIG_*`** — every instrumented object is mapped to the
//! symbols that gate it with the static half's Makefile gate map
//! ([`super::TreeSizes`]: `obj-$(CONFIG_X)`, directory gates, composite
//! parts and their parents). A symbol is **executed** when one of its objects
//! ran, **never executed** when it has instrumented objects and none ran,
//! and **unmeasured** when it has no object of its own (code under
//! `#ifdef CONFIG_X` inside a file that stays — the honest limit, stated).
//!
//! The report ([`CoverageReport`]) is stored beside the row in
//! `.nornir/kernel-coverage/<row>/<sha12>.json`, keyed by the sha256 of the
//! row's merged fragment (the kernel as it SHIPS, without the probe lines).
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow, bail, ensure};
use serde::{Deserialize, Serialize};
use super::{TreeSizes, full};
use crate::kernel::KernelBuild;
pub use nordisk_kernel_evidence::coverage::{COVERAGE_OPT, CoverageReport, GCOV_PROBE_FRAGMENT, KernelProbe, NotRun, StageResult, Status, SymbolCoverage, Totals};
/// The symbols the probe changes (on or off) — never a trim conclusion: what
/// they gate was measured on a kernel that differs from the row's there.
pub const PROBE_ALTERED: &[&str] = &[
"CONFIG_DEBUG_FS",
"CONFIG_DEBUG_FS_ALLOW_ALL",
"CONFIG_DEBUG_FS_DISALLOW_MOUNT",
"CONFIG_DEBUG_FS_ALLOW_NONE",
"CONFIG_GCOV_KERNEL",
"CONFIG_GCOV_PROFILE_ALL",
"CONFIG_CONSTRUCTORS",
"CONFIG_RANDOMIZE_BASE",
"CONFIG_RANDOMIZE_MEMORY",
"CONFIG_RANDOMIZE_MEMORY_PHYSICAL_PADDING",
];
/// The probe of `kernel.coverage` pairs (at most one).
pub fn probe_of_pairs(pairs: &[(String, String)]) -> Result<Option<KernelProbe>> {
ensure!(pairs.len() <= 1, "kernel.{COVERAGE_OPT} given {} times — one probe per build", pairs.len());
match pairs.first() {
None => Ok(None),
Some((_, v)) => KernelProbe::parse(v),
}
}
/// **A probe kernel never reaches a release or a signed medium** — refused by
/// name when the medium is signed with a real root key (`signing_key` set and
/// not `dev`) or its kind is `release`.
pub fn refuse_probe(probe: Option<KernelProbe>, kernel: &str, signing_key: Option<&str>, media_kind: Option<&str>) -> Result<()> {
let Some(p) = probe else { return Ok(()) };
if media_kind.map(str::trim) == Some("release") {
bail!("[kernel-probe-release] kernel '{kernel}': kernel.{COVERAGE_OPT}={} is a DEV probe kernel — refused for a release medium", p.word());
}
if let Some(k) = signing_key.map(str::trim).filter(|k| !k.is_empty() && *k != "dev") {
let _ = k;
bail!(
"[kernel-probe-signed] kernel '{kernel}': kernel.{COVERAGE_OPT}={} is a DEV probe kernel — refused for a medium signed with a real root key (KORP_ROOT_SIGNING_KEY is not `dev`)",
p.word()
);
}
Ok(())
}
/// [`refuse_probe`] over the build's environment: `KORP_ROOT_SIGNING_KEY` and
/// `NORDISK_MEDIA_KIND`.
pub fn refuse_probe_from_env(k: &KernelBuild) -> Result<()> {
let key = std::env::var("KORP_ROOT_SIGNING_KEY").ok();
let kind = std::env::var("NORDISK_MEDIA_KIND").ok();
refuse_probe(k.probe, &k.name, key.as_deref(), kind.as_deref())
}
// ── the kernel image and its gcov list ──────────────────────────────────────
/// **The layout of `struct gcov_info`** in `kernel/gcov/gcc_4_7.c` for one
/// GCC major (x86_64, 8-byte pointers).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GcovLayout {
/// `GCOV_COUNTERS` — the length of `merge[]`.
pub counters: usize,
/// GCC >= 12 adds `checksum` after `stamp` (it fits the padding before
/// `filename`, so the offsets do not move; kept for the record).
pub checksum: bool,
}
impl GcovLayout {
/// The table of `kernel/gcov/gcc_4_7.c` (6.12).
pub fn for_gcc(major: u32) -> Self {
let counters = match major {
15.. => 10,
14 => 9,
10..=13 => 8,
7..=9 => 9,
_ => 10,
};
GcovLayout { counters, checksum: major >= 12 }
}
const FILENAME: u64 = 24;
const MERGE: u64 = 32;
fn n_functions(&self) -> u64 {
Self::MERGE + 8 * self.counters as u64
}
fn functions(&self) -> u64 {
self.n_functions() + 8
}
}
/// The GCC major in a `.comment` text (`GCC: (Debian 14.2.0-19) 14.2.0`).
pub fn gcc_major_of_comment(comment: &[u8]) -> Option<u32> {
let text = String::from_utf8_lossy(comment);
let line = text.split('\0').find(|l| l.starts_with("GCC:"))?;
let last = line.split_whitespace().last()?;
last.split('.').next()?.parse().ok()
}
/// **What the coverage reader needs of `vmlinux`.**
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KernelImage {
/// `_text`.
pub text: u64,
/// `_end`.
pub end: u64,
/// `&gcov_info_head`.
pub head: u64,
/// `&linux_banner` and its bytes in the file (the KASLR check).
pub banner: (u64, Vec<u8>),
/// The gcov layout of the compiler that built it.
pub layout: GcovLayout,
}
impl KernelImage {
/// Read `vmlinux` (ELF, symbols not stripped).
pub fn read(vmlinux: &Path) -> Result<Self> {
let data = std::fs::read(vmlinux).with_context(|| format!("read {}", vmlinux.display()))?;
Self::from_elf(&data).with_context(|| format!("{}", vmlinux.display()))
}
/// From ELF bytes.
pub fn from_elf(data: &[u8]) -> Result<Self> {
use object::{Object, ObjectSection, ObjectSymbol};
let f = object::File::parse(data).map_err(|e| anyhow!("not an ELF: {e}"))?;
let mut want: BTreeMap<String, u64> = BTreeMap::new();
for s in f.symbols() {
if let Ok(n) = s.name() {
if matches!(n, "_text" | "_end" | "gcov_info_head" | "linux_banner") {
want.insert(n.to_string(), s.address());
}
}
}
let get = |n: &str| want.get(n).copied().ok_or_else(|| anyhow!("no `{n}` symbol — not a gcov probe kernel (kernel.coverage=gcov), or a stripped vmlinux"));
let (text, end, head, banner) = (get("_text")?, get("_end")?, get("gcov_info_head")?, get("linux_banner")?);
let mut banner_bytes = Vec::new();
for sec in f.sections() {
if let Ok(Some(d)) = sec.data_range(banner, 64) {
banner_bytes = d.to_vec();
break;
}
}
ensure!(!banner_bytes.is_empty(), "linux_banner at {banner:#x} is in no section with file bytes");
let comment = f.section_by_name(".comment").and_then(|s| s.data().ok()).unwrap_or(&[]);
let major = gcc_major_of_comment(comment).ok_or_else(|| anyhow!("no `GCC:` line in .comment — the gcov layout is GCC's; a clang kernel is not read here"))?;
Ok(KernelImage { text, end, head, banner: (banner, banner_bytes), layout: GcovLayout::for_gcc(major) })
}
}
/// **Kernel memory, by link address.**
pub trait Memory {
/// `len` bytes at virtual address `addr`.
fn read(&self, addr: u64, len: usize) -> Result<&[u8]>;
/// A little-endian u32.
fn u32(&self, addr: u64) -> Result<u32> {
Ok(u32::from_le_bytes(self.read(addr, 4)?.try_into().unwrap()))
}
/// A little-endian u64.
fn u64(&self, addr: u64) -> Result<u64> {
Ok(u64::from_le_bytes(self.read(addr, 8)?.try_into().unwrap()))
}
/// A NUL-terminated string (at most 4096 bytes).
fn cstr(&self, addr: u64) -> Result<String> {
let mut out = Vec::new();
for i in 0..4096u64 {
let b = self.read(addr + i, 1)?[0];
if b == 0 {
return Ok(String::from_utf8_lossy(&out).to_string());
}
out.push(b);
}
bail!("no NUL within 4096 bytes at {addr:#x}")
}
}
/// **A saved kernel image** — the bytes of `[base, base + len)` (`memsave
/// <_text> <_end - _text>` in the QEMU monitor).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageDump {
/// The virtual address of byte 0.
pub base: u64,
/// The bytes.
pub bytes: Vec<u8>,
}
impl ImageDump {
/// Read a dump file saved at `base`.
pub fn read(path: &Path, base: u64) -> Result<Self> {
Ok(ImageDump { base, bytes: std::fs::read(path).with_context(|| format!("read {}", path.display()))? })
}
}
impl Memory for ImageDump {
fn read(&self, addr: u64, len: usize) -> Result<&[u8]> {
let off = addr.checked_sub(self.base).ok_or_else(|| anyhow!("{addr:#x} is below the dump ({:#x})", self.base))? as usize;
self.bytes.get(off..off + len).ok_or_else(|| anyhow!("{addr:#x}+{len} is past the dump ({:#x}+{})", self.base, self.bytes.len()))
}
}
/// **One instrumented object's counters.**
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ObjectCounts {
/// The `.gcda` path the kernel names it by (the build tree's path).
pub gcda: String,
/// Functions with counters.
pub functions: u32,
/// Functions whose arc counters are not all zero.
pub functions_run: u32,
/// Arc counters in the object.
pub counters: u64,
/// The sum of the arc counters (saturating).
pub arc_sum: u64,
}
impl ObjectCounts {
/// Did any of it run?
pub fn ran(&self) -> bool {
self.arc_sum > 0
}
}
/// **Walk the kernel's `gcov_info` list** in `mem`. Refused when the image is
/// not the one `vmlinux` describes (the banner differs: another build, or
/// KASLR moved it) or a pointer leaves the image.
pub fn walk_gcov(img: &KernelImage, mem: &dyn Memory) -> Result<Vec<ObjectCounts>> {
let (baddr, bbytes) = &img.banner;
let live = mem.read(*baddr, bbytes.len()).context("read linux_banner")?;
ensure!(
live == bbytes.as_slice(),
"[kernel-coverage-image] the saved image is not this vmlinux at its link address (linux_banner differs: another build, or KASLR moved the kernel — the probe turns RANDOMIZE_BASE off)"
);
let inside = |p: u64| p >= img.text && p < img.end;
let l = img.layout;
let mut out = Vec::new();
let mut seen = BTreeSet::new();
let mut p = mem.u64(img.head)?;
while p != 0 {
ensure!(inside(p), "gcov_info at {p:#x} is outside the kernel image");
ensure!(seen.insert(p), "the gcov_info list loops at {p:#x}");
ensure!(seen.len() <= 200_000, "more than 200000 gcov_info entries — not a gcov list");
let fname = mem.u64(p + GcovLayout::FILENAME)?;
ensure!(inside(fname), "gcov_info {p:#x}: filename pointer {fname:#x} is outside the image");
let gcda = mem.cstr(fname)?;
ensure!(gcda.ends_with(".gcda"), "gcov_info {p:#x}: filename {gcda:?} is not a .gcda path — wrong layout for this compiler?");
let arcs_active = mem.u64(p + GcovLayout::MERGE)? != 0;
let n = mem.u32(p + l.n_functions())?;
ensure!(n <= 1_000_000, "gcov_info {p:#x} ({gcda}): {n} functions — wrong layout?");
let fns = mem.u64(p + l.functions())?;
let mut c = ObjectCounts { gcda, ..Default::default() };
if arcs_active && n > 0 {
ensure!(inside(fns), "gcov_info {p:#x}: functions pointer {fns:#x} is outside the image");
for i in 0..u64::from(n) {
let fi = mem.u64(fns + 8 * i)?;
if fi == 0 || !inside(fi) || mem.u64(fi)? != p {
// NULL, or a COMDAT copy keyed to another object.
continue;
}
let num = mem.u32(fi + 24)?;
ensure!(num <= 10_000_000, "{}: function {i}: {num} arc counters — wrong layout?", c.gcda);
if num == 0 {
continue;
}
let values = mem.u64(fi + 32)?;
ensure!(inside(values), "{}: function {i}: counters at {values:#x} are outside the image", c.gcda);
let bytes = mem.read(values, num as usize * 8)?;
let sum = bytes.chunks_exact(8).fold(0u64, |a, b| a.saturating_add(u64::from_le_bytes(b.try_into().unwrap())));
c.functions += 1;
c.counters += u64::from(num);
if sum > 0 {
c.functions_run += 1;
}
c.arc_sum = c.arc_sum.saturating_add(sum);
}
}
out.push(c);
p = mem.u64(p + 8)?;
}
Ok(out)
}
/// The linked object a `.gcda` path names: the longest path suffix of
/// `<stem>.o` that is a linked object.
pub fn object_of_gcda(gcda: &str, linked: &BTreeSet<String>) -> Option<String> {
let stem = gcda.strip_suffix(".gcda")?;
let o = format!("{stem}.o");
let mut rest = o.as_str();
loop {
if linked.contains(rest) {
return Some(rest.to_string());
}
rest = &rest[rest.find('/')? + 1..];
}
}
/// Every symbol gating one linked object — the ONE resolver
/// ([`crate::kbuild_gates::ObjectGates::gates_of`]), the same answer the
/// syscall join gets. It used to take the UNION of every line listing the
/// object; a gate is what every way in needs (KTOOL-W2 fix 14).
pub fn linked_gates(sizes: &TreeSizes, obj: &str) -> BTreeSet<String> {
sizes.gates_of(obj)
}
// ── the report ──────────────────────────────────────────────────────────────
/// One saved image: which node, after which stage.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
/// The VM (`cp`, `worker`).
pub node: String,
/// The suite stage it was saved after (`ready`, `bridge-checks`).
pub stage: String,
/// The counters read from it.
pub counts: Vec<ObjectCounts>,
}
/// What a coverage run measured, before it becomes a report.
pub struct Measured<'a> {
/// The row.
pub row: &'a str,
/// The kernel declaration.
pub kernel: &'a KernelBuild,
/// The row's merged fragment, as it ships.
pub fragment: &'a str,
/// The probe kernel's built `.config`.
pub probe_config: &'a str,
/// The static half's sizes of the probe tree.
pub sizes: &'a TreeSizes,
/// The snapshots, in the order the suite saved them.
pub snapshots: &'a [Snapshot],
/// The suite.
pub suite: &'a str,
/// The stages.
pub stages: Vec<StageResult>,
/// The checks not run.
pub not_run: Vec<NotRun>,
/// The tool line.
pub tool: String,
}
/// **Map the counters to `CONFIG_*` symbols** — the report.
pub fn report(m: Measured<'_>) -> Result<CoverageReport> {
let probe = m.kernel.probe.ok_or_else(|| anyhow!("kernel '{}' is not a probe kernel (kernel.coverage=gcov)", m.kernel.name))?;
let cfg = crate::kconfig_graph::ConfigView::parse(m.probe_config);
ensure!(cfg.is_set("GCOV_KERNEL"), "[kernel-coverage-no-gcov] the probe kernel's .config has no CONFIG_GCOV_KERNEL=y — the probe fragment did not land");
let linked: BTreeSet<String> = m.sizes.objects.keys().cloned().collect();
// Per object: run? and in which node:stage its counters rose.
#[derive(Default)]
struct Obj {
instrumented: bool,
ran: bool,
covered_by: BTreeSet<String>,
}
let mut objs: BTreeMap<String, Obj> = BTreeMap::new();
let mut last: BTreeMap<(String, String), u64> = BTreeMap::new();
let mut labels = Vec::new();
for snap in m.snapshots {
let label = format!("{}:{}", snap.node, snap.stage);
labels.push(label.clone());
for c in &snap.counts {
let Some(o) = object_of_gcda(&c.gcda, &linked) else { continue };
let e = objs.entry(o.clone()).or_default();
if c.counters > 0 {
e.instrumented = true;
}
let prev = last.insert((snap.node.clone(), o), c.arc_sum).unwrap_or(0);
if c.arc_sum > prev {
e.ran = true;
e.covered_by.insert(label.clone());
}
}
}
// Per symbol: the objects it gates.
let mut gated: BTreeMap<String, Vec<&str>> = BTreeMap::new();
for o in &linked {
for g in linked_gates(m.sizes, o) {
gated.entry(g).or_default().push(o.as_str());
}
}
let altered: BTreeSet<&str> = PROBE_ALTERED.iter().copied().collect();
let mut symbols = Vec::new();
let mut totals = Totals { objects: linked.len(), ..Default::default() };
totals.instrumented = objs.values().filter(|o| o.instrumented).count();
totals.objects_run = objs.values().filter(|o| o.ran).count();
for (k, v) in &cfg.values {
if v != "y" {
continue;
}
let mine = gated.get(k).cloned().unwrap_or_default();
let mut sc = SymbolCoverage {
symbol: full(k),
status: Status::Unmeasured,
objects: mine.len(),
instrumented: 0,
objects_run: 0,
covered_by: Vec::new(),
bytes: mine.iter().map(|o| m.sizes.objects.get(*o).copied().unwrap_or(0)).sum(),
probe_altered: altered.contains(full(k).as_str()),
};
let mut by = BTreeSet::new();
for o in &mine {
if let Some(ob) = objs.get(*o) {
if ob.instrumented {
sc.instrumented += 1;
}
if ob.ran {
sc.objects_run += 1;
by.extend(ob.covered_by.iter().cloned());
}
}
}
sc.status = if sc.objects_run > 0 {
Status::Executed
} else if sc.instrumented > 0 {
Status::NeverExecuted
} else {
Status::Unmeasured
};
// Stage order, not alphabetical.
sc.covered_by = labels.iter().filter(|l| by.contains(*l)).cloned().collect();
match sc.status {
Status::Executed => totals.executed += 1,
Status::NeverExecuted => totals.never_executed += 1,
Status::Unmeasured => totals.unmeasured += 1,
}
totals.symbols += 1;
symbols.push(sc);
}
let never_run_objects = objs.iter().filter(|(_, o)| o.instrumented && !o.ran).map(|(k, _)| k.clone()).collect();
Ok(CoverageReport {
schema: CoverageReport::SCHEMA.to_string(),
row: m.row.to_string(),
kernel: m.kernel.name.clone(),
version: m.kernel.version.clone(),
fragment_sha256: nornir_hash::sha256_hex(m.fragment.as_bytes()),
probe_fragment_sha256: nornir_hash::sha256_hex(probe.fragment().as_bytes()),
probe,
measured_on: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
suite: m.suite.to_string(),
stages: m.stages,
not_run: m.not_run,
snapshots: labels,
tool: m.tool,
totals,
symbols,
never_run_objects,
})
}
/// `<repo>/.nornir/kernel-coverage/<row>/`.
pub fn coverage_dir(repo_root: &Path, row: &str) -> PathBuf {
repo_root.join(".nornir").join("kernel-coverage").join(row)
}
/// Where the report of the row's fragment `sha` lives.
pub fn report_path(repo_root: &Path, row: &str, fragment_sha: &str) -> PathBuf {
coverage_dir(repo_root, row).join(format!("{}.json", &fragment_sha[..12.min(fragment_sha.len())]))
}
/// Write a report beside the row; returns its path.
pub fn store(repo_root: &Path, r: &CoverageReport) -> Result<PathBuf> {
let p = report_path(repo_root, &r.row, &r.fragment_sha256);
std::fs::create_dir_all(p.parent().unwrap()).with_context(|| format!("create {}", p.parent().unwrap().display()))?;
let mut text = serde_json::to_string_pretty(r)?;
text.push('\n');
std::fs::write(&p, text).with_context(|| format!("write {}", p.display()))?;
Ok(p)
}
/// The report measured on THIS fragment, if there is one.
pub fn load_for(repo_root: &Path, row: &str, fragment: &str) -> Result<Option<CoverageReport>> {
let sha = nornir_hash::sha256_hex(fragment.as_bytes());
let p = report_path(repo_root, row, &sha);
if !p.is_file() {
return Ok(None);
}
let r: CoverageReport = serde_json::from_str(&std::fs::read_to_string(&p).with_context(|| format!("read {}", p.display()))?).with_context(|| format!("parse {}", p.display()))?;
ensure!(r.fragment_sha256 == sha, "{}: measured on fragment {}, not {sha}", p.display(), r.fragment_sha256);
Ok(Some(r))
}
// ── the protect list ────────────────────────────────────────────────────────
/// **The row's protect list** — error and recovery paths a suite that never
/// fails cannot exercise (block errors, watchdogs, NVMe reset): never a veto
/// candidate, whatever the coverage says. `<repo>/.nornir/kernel-vetoes/<row>/protect.list`,
/// one `PATTERN reason…` per line (`*` matches any run of characters); the
/// probe's own symbols ([`PROBE_ALTERED`]) are always protected.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProtectList {
/// `(pattern, reason)`.
pub rows: Vec<(String, String)>,
}
impl ProtectList {
/// Parse the file text.
pub fn parse(text: &str) -> Result<Self> {
let mut rows = Vec::new();
for (i, line) in text.lines().enumerate() {
let l = line.trim();
if l.is_empty() || l.starts_with('#') {
continue;
}
let (pat, why) = l.split_once(char::is_whitespace).unwrap_or((l, ""));
ensure!(pat.starts_with("CONFIG_"), "protect.list line {}: {pat:?} is not a CONFIG_* pattern", i + 1);
rows.push((pat.to_string(), why.trim().to_string()));
}
Ok(ProtectList { rows })
}
/// The row's list (empty when the file is absent).
pub fn load(repo_root: &Path, row: &str) -> Result<Self> {
let p = super::veto::veto_dir(repo_root, row).join("protect.list");
match std::fs::read_to_string(&p) {
Ok(t) => Self::parse(&t).with_context(|| format!("{}", p.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
Err(e) => Err(e).with_context(|| format!("read {}", p.display())),
}
}
/// Why `sym` is protected, if it is.
pub fn why(&self, sym: &str) -> Option<String> {
let f = full(sym);
if PROBE_ALTERED.contains(&f.as_str()) {
return Some("changed by the coverage probe itself".to_string());
}
self.rows.iter().find(|(p, _)| glob(p, &f)).map(|(p, w)| if w.is_empty() { p.clone() } else { format!("{p}: {w}") })
}
}
/// `*`-glob.
pub fn glob(pat: &str, s: &str) -> bool {
let parts: Vec<&str> = pat.split('*').collect();
if parts.len() == 1 {
return pat == s;
}
let mut rest = s;
for (i, p) in parts.iter().enumerate() {
if i == 0 {
if !rest.starts_with(p) {
return false;
}
rest = &rest[p.len()..];
} else if i == parts.len() - 1 {
return rest.ends_with(p);
} else if let Some(at) = rest.find(p) {
rest = &rest[at + p.len()..];
} else {
return false;
}
}
true
}
#[cfg(test)]
mod tests;