extern crate glob;
use clap::Args;
use content_inspector::{ContentType, inspect};
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 matches: Vec<_> = line.match_indices("$RANDOM").collect();
if matches.is_empty() {
return false;
}
for m in &matches {
let start = m.0;
let next = start + "$RANDOM".len();
if start >= 3 && line[start - 3..start] == *"$$-" {
return false;
}
if next + 2 < line.len() && line[next..next + 3] == *"-$$" {
return false;
}
if next < line.len() {
if let Some(ch) = line.chars().nth(next) {
if ch.is_ascii_uppercase() || ch == '_' {
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}"
);
}
}
}
}
let mut skipext: Vec<String> = vec![];
skipext.push(".orig".to_string());
skipext.push("~".to_string());
for ext in [
"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",
"page",
"php",
"pl",
"png",
"po",
"properties",
"py",
"py",
"rb",
"result",
"svg",
"test",
"tfm",
"ts",
"txt",
"vf",
"xml",
"xpm",
] {
skipext.push(format!(".{ext}"));
}
let mut patched = vec![];
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.push(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.into()) {
continue 'nextfile;
}
}
let path = entry.path();
let mpath = path.strip_prefix("./").unwrap();
for g in &skipglob {
if g.matches_path(mpath) {
continue 'nextfile;
}
}
let mut file = fs::File::open(path)?;
let mut buf = [0; 1024];
let n = file.read(&mut buf)?;
if !buf.starts_with(b"#!") {
continue 'nextfile;
}
let binsh = b"/bin/sh";
let mut lines = buf.splitn(2, |ch| *ch == b'\n');
let first = lines.next().unwrap();
if !first.windows(binsh.len()).any(|win| win == binsh) {
continue 'nextfile;
}
if inspect(&buf[..n]) == ContentType::UTF_8 {
let file = fs::File::open(path)?;
let reader = BufReader::new(file);
for (i, line) in reader.lines().enumerate() {
if let Ok(line) = line {
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,
line
);
print_random_warning();
}
if check_test_eq(line) {
eprintln!(
"ERROR: [check-portability] => Found test ... == ...:"
);
eprintln!(
"ERROR: [check-portability] {}:{}: {}",
mpath.display(),
i + 1,
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"));
}
}