use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub fn refuse_decryption_envelopes(
src: &str,
path: &str,
include_dirs: &[String],
) -> Result<(), String> {
let mut seen = HashSet::new();
scan(src, Path::new(path), include_dirs, &mut seen, 0)
}
const MAX_DEPTH: usize = 16;
fn scan(
src: &str,
path: &Path,
include_dirs: &[String],
seen: &mut HashSet<PathBuf>,
depth: usize,
) -> Result<(), String> {
if (!src.contains("protect") && !src.contains("include")) || depth > MAX_DEPTH {
return Ok(());
}
let b = src.as_bytes();
let mut i = 0;
let mut line = 1usize;
while i < b.len() {
match b[i] {
b'\n' => {
line += 1;
i += 1;
}
b'/' if b.get(i + 1) == Some(&b'/') => i = line_end(b, i),
b'/' if b.get(i + 1) == Some(&b'*') => {
i += 2;
while i < b.len() && !(b[i] == b'*' && b.get(i + 1) == Some(&b'/')) {
line += usize::from(b[i] == b'\n');
i += 1;
}
i = (i + 2).min(b.len());
}
b'"' => i = skip_string(b, i, &mut line),
b'`' => {
let (word, next) = ident(b, i + 1);
i = next;
match word {
"pragma" => {
let (kw, after) = ident(b, skip_blank(b, i));
if kw == "protect"
&& opens_decryption_envelope(&src[after..line_end(b, after)])
{
return Err(format!(
"{}:{line}: protected envelope (`pragma protect \
begin_protected): decrypting compilation is not supported \
(LRM 1800-2023 34.3.2): vj has no decryption keys and \
slang would skip the encrypted text silently",
path.display()
));
}
}
"include" => {
let at = skip_blank(b, i);
if b.get(at) == Some(&b'"') {
let close = b[at + 1..].iter().position(|&c| c == b'"' || c == b'\n');
if let Some(n) = close.filter(|&n| b[at + 1 + n] == b'"') {
let name = &src[at + 1..at + 1 + n];
if let Some(file) = find_include(name, path, include_dirs)
&& seen.insert(file.clone())
&& let Ok(text) = std::fs::read_to_string(&file)
{
scan(&text, &file, include_dirs, seen, depth + 1)?;
}
}
}
}
_ => {}
}
}
_ => i += 1,
}
}
Ok(())
}
fn opens_decryption_envelope(rest: &str) -> bool {
let b = rest.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] == b'"' {
i = skip_string(b, i, &mut 0);
} else if b[i] == b'/' && b.get(i + 1) == Some(&b'/') {
return false;
} else if b[i].is_ascii_alphabetic() || b[i] == b'_' {
let (word, next) = ident(b, i);
if word == "begin_protected" {
return true;
}
i = next;
} else {
i += 1;
}
}
false
}
fn find_include(name: &str, from: &Path, include_dirs: &[String]) -> Option<PathBuf> {
let own = from.parent().unwrap_or(Path::new("."));
std::iter::once(own.to_path_buf())
.chain(include_dirs.iter().map(PathBuf::from))
.map(|d| d.join(name))
.find(|p| p.is_file())
}
fn ident(b: &[u8], at: usize) -> (&str, usize) {
let mut e = at;
while e < b.len() && (b[e].is_ascii_alphanumeric() || b[e] == b'_' || b[e] == b'$') {
e += 1;
}
(std::str::from_utf8(&b[at..e]).unwrap_or(""), e)
}
fn skip_blank(b: &[u8], mut i: usize) -> usize {
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
i += 1;
}
i
}
fn line_end(b: &[u8], mut i: usize) -> usize {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
i
}
fn skip_string(b: &[u8], mut i: usize, line: &mut usize) -> usize {
i += 1;
while i < b.len() && b[i] != b'"' {
if b[i] == b'\\' {
i += 1;
}
if i < b.len() && b[i] == b'\n' {
*line += 1;
}
i += 1;
}
(i + 1).min(b.len())
}
#[cfg(test)]
mod tests {
use super::*;
fn refused(src: &str) -> bool {
refuse_decryption_envelopes(src, "t.sv", &[]).is_err()
}
#[test]
fn begin_protected_is_refused_with_its_line() {
let e = refuse_decryption_envelopes(
"module m;\n`pragma protect data_method=\"x\", begin_protected\nendmodule\n",
"t.sv",
&[],
)
.unwrap_err();
assert!(e.starts_with("t.sv:2:") && e.contains("34.3.2"), "{e}");
}
#[test]
fn encryption_envelopes_comments_and_strings_pass() {
assert!(!refused(
"`pragma protect begin\nwire w;\n`pragma protect end\n"
));
assert!(!refused("// `pragma protect begin_protected\nwire w;\n"));
assert!(!refused("/* `pragma protect begin_protected */ wire w;\n"));
assert!(!refused(
"initial $display(\"`pragma protect begin_protected\");\n"
));
assert!(!refused("`pragma protect comment=\"begin_protected\"\n"));
assert!(!refused("`pragma once\n"));
}
}