// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne
//! The Imprint entry of the Info menu (`skills/rust-info-imprint`).
//!
//! The text is ndaal's reviewed legal notice, word for word as on
//! sbom.ndaal.eu 1.6.32. It is German in every UI language and the page marks
//! it `lang="de"`, so a screen reader switches voice on an English page.
use crate::routes::layout::html_escape;
/// `GET /imprint`.
pub const IMPRINT_ROUTE: &str = "/imprint";
/// `<title>` and `<h1>`: the German word, in every UI language.
pub const IMPRINT_HEADING: &str = "Impressum";
/// One section of the legal notice.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ImprintSection {
/// Section heading (`<h2>`).
pub heading: &'static str,
/// The section's lines and paragraphs, one `<p>` each.
pub paragraphs: &'static [&'static str],
}
/// ndaal's reviewed legal notice, up to the product sections. Change it only
/// after a legal review, and update the pin in
/// `the_reviewed_text_is_unchanged` in the same commit.
pub const REVIEWED_SECTIONS: &[ImprintSection] = &[
ImprintSection {
heading: "Impressum / Herausgeber",
paragraphs: &[
"Verantwortlicher im Sinne des § 10 Absatz 3 Mediendienste-Staatsvertrag",
"ndaal GmbH & Co. KG",
"Geschäftsführung",
"Carsten Dingendahl",
"Die ndaal GmbH & Co. KG ist als Inhaltsanbieter nach § 8 Teledienstegesetz für die »eigenen Inhalte«, die sie zur Nutzung bereithält, verantwortlich. Bei Problemen oder Fragen sollten Sie sich direkt an unseren Datenschutzbeauftragten wenden, der Ihnen gerne weiterhilft.",
],
},
ImprintSection {
heading: "Anschrift",
paragraphs: &[
"ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG",
"Christophstr. 15-17",
"50670 Köln",
"Deutschland",
"Tel.: +49.221.65 086 200",
"E-Mail: info@ndaal.eu",
"Website: www.ndaal.eu",
],
},
ImprintSection {
heading: "Firmensitz / Registergericht",
paragraphs: &[
"Köln / Amtsgericht K, HRA 35474",
"Umsatzsteuer Identifikationsnummer: DE329537036",
],
},
ImprintSection {
heading: "Komplementärin",
paragraphs: &[
"XSTeam Beteiligungsgesellschaft mbH",
"Geschäftsführung: Carsten Dingendahl",
"Firmensitz / Registergericht: Köln / Amtsgericht K, HRB 105499",
"E-Mail: info@xsteam.eu",
],
},
ImprintSection {
heading: "Haftung für Inhalte",
paragraphs: &[
"Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.",
],
},
ImprintSection {
heading: "Haftung für Links",
paragraphs: &[
"Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.",
],
},
ImprintSection {
heading: "Urheberrecht",
paragraphs: &[
"Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.",
],
},
];
/// The product's own sections, shown before "Weitere Profile". Empty here:
/// csaf-crud has none. They are the product's text, outside the reviewed pin.
pub const PRODUCT_SECTIONS: &[ImprintSection] = &[];
/// The closing section of the reviewed notice.
pub const PROFILES_SECTION: ImprintSection = ImprintSection {
heading: "Weitere Profile",
paragraphs: &[
"Dieses Impressum gilt für weitere Profile:",
"linkedin.com",
"xing.com",
],
};
/// The sections in page order: the reviewed text, the product's sections,
/// then "Weitere Profile".
#[must_use]
pub fn imprint_sections() -> Vec<ImprintSection> {
REVIEWED_SECTIONS
.iter()
.chain(PRODUCT_SECTIONS)
.chain([&PROFILES_SECTION])
.copied()
.collect()
}
/// The page body: the heading and every section inside one `lang="de"`
/// container. Every string is HTML-escaped.
#[must_use]
pub fn imprint_body_html() -> String {
let sections = imprint_sections()
.iter()
.map(section_html)
.collect::<String>();
format!(
"<div lang=\"de\">\n<h1>{IMPRINT_HEADING}</h1>\n<div class=\"card\">\n{sections}</div>\n</div>"
)
}
/// One section: its `<h2>` and one `<p>` per line, all escaped.
fn section_html(section: &ImprintSection) -> String {
let mut paragraphs = String::new();
for text in section.paragraphs {
paragraphs.push_str(" <p>");
paragraphs.push_str(&html_escape(text));
paragraphs.push_str("</p>\n");
}
format!(" <h2>{}</h2>\n{paragraphs}", html_escape(section.heading))
}
#[cfg(test)]
mod tests {
use super::{
IMPRINT_HEADING, IMPRINT_ROUTE, PROFILES_SECTION, REVIEWED_SECTIONS, imprint_body_html,
imprint_sections,
};
use crate::routes::layout::html_escape;
/// FNV-1a, 64 bit: a stable fingerprint of the text, so any edit shows.
/// A tripwire, not a security measure.
fn fnv1a(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
})
}
/// Every heading and paragraph of the reviewed text, in order, one per
/// line: the reviewed sections and "Weitere Profile".
fn reviewed_text() -> String {
REVIEWED_SECTIONS
.iter()
.chain([&PROFILES_SECTION])
.flat_map(|section| {
std::iter::once(section.heading).chain(section.paragraphs.iter().copied())
})
.collect::<Vec<_>>()
.join("\n")
}
/// Route and heading are fixed by ndaal-design 8.1 and `rust-info-imprint`.
#[test]
fn entry_matches_the_info_menu_design() {
assert_eq!(IMPRINT_ROUTE, "/imprint");
assert_eq!(IMPRINT_HEADING, "Impressum");
assert!(imprint_body_html().contains("Impressum</h1>"));
}
/// The text is ndaal's reviewed legal notice. Any edit, even one
/// character, fails here: change the text only after a legal review, and
/// update this pin in the same commit. The pin was computed the same way
/// from the live sbom.ndaal.eu 1.6.32 page on 2026-10-11.
#[test]
fn the_reviewed_text_is_unchanged() {
assert_eq!(
fnv1a(reviewed_text().as_bytes()),
2_622_182_834_256_377_760,
"the reviewed imprint text changed"
);
}
/// The German text sits in a `lang="de"` container on a page whose
/// language may be another one, so a screen reader switches voice
/// (WCAG 2.2 success criterion 3.1.2, Language of Parts).
#[test]
fn the_german_text_is_marked_as_german() {
let html = imprint_body_html();
let marker = html
.find("<div lang=\"de\">")
.expect("a lang=\"de\" container");
let heading = html.find("Impressum</h1>").expect("the heading");
let last = html.find("xing.com").expect("the last line");
assert!(
marker < heading && heading < last,
"the container holds the whole notice"
);
}
/// Every section heading and every line reaches the page, escaped, in
/// order, and "Weitere Profile" closes the notice.
#[test]
fn every_section_and_line_is_shown_in_order() {
let html = imprint_body_html();
let sections = imprint_sections();
let mut from = 0;
for section in §ions {
for text in std::iter::once(section.heading).chain(section.paragraphs.iter().copied()) {
let shown = html_escape(text);
let at = html[from..]
.find(&shown)
.unwrap_or_else(|| panic!("missing or out of order: {shown}"));
from += at + shown.len();
}
}
assert_eq!(html.matches("<h2").count(), sections.len(), "one <h2> each");
assert_eq!(
sections.last(),
Some(&PROFILES_SECTION),
"Weitere Profile closes the notice"
);
}
/// Markup in the fixed wording must stay text.
#[test]
fn the_notice_is_escaped() {
let html = imprint_body_html();
assert!(html.contains("ndaal GmbH & Co. KG"));
assert!(!html.contains("ndaal GmbH & Co. KG"));
}
}