sccp-protocol 0.3.4

Cisco SCCP (Skinny Client Control Protocol) codec/server
Documentation
use std::fs;
use std::path::{Path, PathBuf};

const FORBIDDEN_PRODUCTION_PATTERNS: &[(&str, &str)] = &[
    ("format!(\"<", "formatted XML construction"),
    ("format!(r#\"<", "formatted raw XML construction"),
    ("push_str(\"<", "incremental XML construction"),
    ("write!(\"<", "formatted XML writer construction"),
    (".find(\"<", "manual XML tag search"),
    (".split(\"<", "manual XML tokenization"),
    (
        ".contains(\"<CiscoIPPhone",
        "manual known-document detection",
    ),
    (".replace(\"&\"", "manual XML escaping"),
];

const FORBIDDEN_COMPACT_PATTERNS: &[(&str, &str)] = &[
    ("format!(\"<", "formatted XML construction"),
    ("format!(r#\"<", "formatted raw XML construction"),
    ("push_str(\"<", "incremental XML construction"),
    (",\"<CiscoIPPhone", "formatted XML writer construction"),
    (".find(\"<", "manual XML tag search"),
    (".split(\"<", "manual XML tokenization"),
    (
        ".contains(\"<CiscoIPPhone",
        "manual known-document detection",
    ),
    (".replace(\"&\"", "manual XML escaping"),
];

fn rust_sources(root: &Path, output: &mut Vec<PathBuf>) {
    let mut entries = fs::read_dir(root)
        .unwrap_or_else(|error| panic!("unable to read {}: {error}", root.display()))
        .collect::<Result<Vec<_>, _>>()
        .unwrap_or_else(|error| panic!("unable to enumerate {}: {error}", root.display()));
    entries.sort_by_key(|entry| entry.path());
    for entry in entries {
        let path = entry.path();
        if path.is_dir() {
            rust_sources(&path, output);
        } else if path.extension().is_some_and(|extension| extension == "rs") {
            output.push(path);
        }
    }
}

#[test]
fn production_code_uses_the_typed_xml_boundary() {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let workspace = manifest
        .parent()
        .expect("sccp-protocol crate must have a workspace parent");
    let mut sources = Vec::new();
    for root in [manifest.join("src"), workspace.join("asterisk-module/src")] {
        rust_sources(&root, &mut sources);
    }

    let mut violations = Vec::new();
    for path in sources {
        let source = fs::read_to_string(&path)
            .unwrap_or_else(|error| panic!("unable to read {}: {error}", path.display()));
        let production = source
            .split_once("#[cfg(test)]")
            .map_or(source.as_str(), |(production, _)| production);
        for (line_index, line) in production.lines().enumerate() {
            for (pattern, reason) in FORBIDDEN_PRODUCTION_PATTERNS {
                if line.contains(pattern) {
                    violations.push(format!(
                        "{}:{}: {reason}: {line}",
                        path.display(),
                        line_index + 1,
                    ));
                }
            }
        }
        let compact = production
            .chars()
            .filter(|character| !character.is_ascii_whitespace())
            .collect::<String>();
        for (pattern, reason) in FORBIDDEN_COMPACT_PATTERNS {
            if compact.contains(pattern) {
                violations.push(format!("{}: {reason}", path.display()));
            }
        }
    }

    assert!(
        violations.is_empty(),
        "known phone XML must use typed Serde models and the shared XML codec:\n{}",
        violations.join("\n"),
    );
}