extern crate glob;
use crate::scrub_ctrl;
use clap::Args;
use std::collections::HashSet;
use std::fs;
use std::io::{BufRead, BufReader, Read};
use walkdir::WalkDir;
#[derive(Args, Debug)]
pub struct Cmd {}
fn check_random(line: &str) -> bool {
let mut rv = false;
let bytes = line.as_bytes();
for (start, _) in line.match_indices("$RANDOM") {
let next = start + "$RANDOM".len();
if start >= 3 && &bytes[start - 3..start] == b"$$-" {
return false;
}
if bytes.get(next..next + 3) == Some(b"-$$") {
return false;
}
if let Some(&b) = bytes.get(next) {
if b.is_ascii_uppercase() || b == b'_' {
continue;
}
}
rv = true;
}
rv
}
fn check_test_eq(line: &str) -> bool {
let words: Vec<_> = line.split_whitespace().collect();
let mut idx = 2;
while idx < words.len() {
if words[idx] == "=="
&& (words[idx - 2] == "test" || words[idx - 2] == "[")
{
return true;
}
idx += 1;
}
false
}
fn print_random_warning() {
let msg = r#"
Explanation:
===========================================================================
The variable $RANDOM is not required for a POSIX-conforming shell, and
many implementations of /bin/sh do not support it. It should therefore
not be used in shell programs that are meant to be portable across a
large number of POSIX-like systems.
===========================================================================
"#;
println!("{msg}");
}
fn print_test_eq_error() {
let msg = r#"
Explanation:
===========================================================================
The "test" command, as well as the "[" command, are not required to know
the "==" operator. Only a few implementations like bash and some
versions of ksh support it.
When you run "test foo == foo" on a platform that does not support the
"==" operator, the result will be "false" instead of "true". This can
lead to unexpected behavior.
There are two ways to fix this error message. If the file that contains
the "test ==" is needed for building the package, you should create a
patch for it, replacing the "==" operator with "=". If the file is not
needed, add its name to the CHECK_PORTABILITY_SKIP variable in the
package Makefile.
===========================================================================
"#;
println!("{msg}");
}
impl Cmd {
pub fn run(&self) -> Result<i32, Box<dyn std::error::Error>> {
let mut rv = 0;
let mut skipglob = vec![];
if let Ok(paths) = std::env::var("CHECK_PORTABILITY_SKIP") {
for p in paths.split_whitespace().collect::<Vec<&str>>() {
match glob::Pattern::new(p) {
Ok(g) => skipglob.push(g),
Err(e) => {
eprintln!(
"WARNING: invalid CHECK_PORTABILITY_SKIP glob '{p}': {e}"
);
}
}
}
}
const SKIPEXT: &[&str] = &[
"~",
".1",
".3",
".C",
".a",
".ac",
".c",
".cc",
".css",
".cxx",
".docbook",
".dtd",
".el",
".f",
".gif",
".gn",
".go",
".gz",
".h",
".hpp",
".htm",
".html",
".hxx",
".idl",
".inc",
".jpg",
".js",
".json",
".kicad_mod",
".m4",
".map",
".md",
".mo",
".ogg",
".orig",
".page",
".php",
".pl",
".png",
".po",
".properties",
".py",
".rb",
".result",
".svg",
".test",
".tfm",
".ts",
".txt",
".vf",
".xml",
".xpm",
];
let mut patched: HashSet<String> = HashSet::new();
if let Ok(patchdir) = std::env::var("PATCHDIR") {
for patch in
WalkDir::new(patchdir).into_iter().filter_map(|e| e.ok())
{
if !patch.file_type().is_file() {
continue;
}
if !patch.file_name().to_string_lossy().starts_with("patch-") {
continue;
}
let pfile = fs::File::open(patch.path())?;
let reader = BufReader::new(pfile);
for line in reader.lines() {
let line = line?;
if line.starts_with("+++") {
let v: Vec<&str> =
line.splitn(2, char::is_whitespace).collect();
if v.len() == 2 {
patched.insert(v[1].to_string());
}
break;
}
}
}
}
'nextfile: for entry in
WalkDir::new(".").into_iter().filter_map(|e| e.ok())
{
if !entry.file_type().is_file() {
continue;
}
let fname: &str = &entry.file_name().to_string_lossy();
for ext in SKIPEXT {
if fname.ends_with(ext) {
continue 'nextfile;
}
}
if let Some(p) =
entry.file_name().to_string_lossy().strip_suffix(".in")
{
if patched.contains(p) {
continue 'nextfile;
}
}
let path = entry.path();
let mpath = match path.strip_prefix("./") {
Ok(p) => p,
Err(_) => path,
};
for g in &skipglob {
if g.matches_path(mpath) {
continue 'nextfile;
}
}
let file = fs::File::open(path)?;
let mut reader = BufReader::with_capacity(1024, file);
let head = reader.fill_buf()?;
if !head.starts_with(b"#!") {
continue 'nextfile;
}
let binsh = b"/bin/sh";
let Some(newline) = head.iter().position(|&c| c == b'\n') else {
continue 'nextfile;
};
let first = &head[..newline];
if !first.windows(binsh.len()).any(|win| win == binsh) {
continue 'nextfile;
}
for (i, line) in reader.by_ref().lines().enumerate() {
let Ok(line) = line else {
continue;
};
let line = line.trim();
if line.starts_with('#') {
continue;
}
if check_random(line) {
eprintln!("WARNING: [check-portability] => Found $RANDOM:");
eprintln!(
"WARNING: [check-portability] {}:{}: {}",
mpath.display(),
i + 1,
scrub_ctrl(line)
);
print_random_warning();
}
if check_test_eq(line) {
eprintln!(
"ERROR: [check-portability] => Found test ... == ...:"
);
eprintln!(
"ERROR: [check-portability] {}:{}: {}",
mpath.display(),
i + 1,
scrub_ctrl(line)
);
print_test_eq_error();
rv = 1;
}
}
}
Ok(rv)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_random() {
assert!(check_random("$RANDOM"));
assert!(check_random("-$RANDOM"));
assert!(check_random("$-$RANDOM"));
assert!(check_random("$RANDOM-"));
assert!(check_random("$RANDOM-$"));
assert!(!check_random("$$-$RANDOM"));
assert!(!check_random("$RANDOM-$$"));
assert!(!check_random("$RANDOM-$$ $RANDOM"));
assert!(!check_random("$RANDOM $RANDOM-$$"));
assert!(!check_random("$RANDOMIZE"));
assert!(!check_random("$RANDOM_ISH"));
assert!(check_random("$RANDOMIZE $RANDOM"));
assert!(!check_random(""));
assert!(!check_random("RANDOM"));
assert!(!check_random("$ RANDOM"));
}
#[test]
fn test_eq() {
assert!(check_test_eq("if [ foo == bar ]; then"));
assert!(!check_test_eq("if [ 'foo bar' == ojnk ]; then"));
assert!(!check_test_eq(""));
assert!(!check_test_eq("foo == bar"));
assert!(!check_test_eq("if foo == bar"));
assert!(!check_test_eq("if [ foo = bar ]; then"));
}
}