rua 0.19.10

Secure jailed AUR helper for Arch Linux
use crate::terminal_util;
extern crate libflate;
extern crate ruzstd;
use colored::*;
use indexmap::IndexSet;
use libflate::gzip::Decoder;
use log::debug;
use ruzstd::StreamingDecoder;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use tar::*;
use xz2::read::XzDecoder;

pub fn tar_check_unwrap(tar_file: &Path, file_name: &str) {
	let result = tar_check(tar_file, file_name);
	result.unwrap_or_else(|err| {
		eprintln!("{}", err);
		std::process::exit(1)
	})
}

pub fn tar_check(tar_file: &Path, tar_str: &str) -> Result<(), String> {
	let archive = File::open(tar_file).unwrap_or_else(|_| panic!("cannot open file {}", tar_str));
	debug!("Checking file {}", tar_str);
	if tar_str.ends_with(".tar") {
		tar_check_archive(Archive::new(archive), tar_str);
		Ok(())
	} else if tar_str.ends_with(".tar.xz") || tar_str.ends_with(".tar.lzma") {
		tar_check_archive(Archive::new(XzDecoder::new(archive)), tar_str);
		Ok(())
	} else if tar_str.ends_with(".tar.gz") || tar_str.ends_with(".tar.gzip") {
		match Decoder::new(archive) {
			Ok(decoded) => {
				tar_check_archive(Archive::new(decoded), tar_str);
				Ok(())
			},
			Err(err) => {
				Err(format!("File {:?} seems to be corrupted, could not decode the gzip contents. Underlying libflate error: {}", tar_file, err))
			},
		}
	} else if tar_str.ends_with(".tar.zst") || tar_str.ends_with(".tar.zstd") {
		let mut archive = archive;
		match StreamingDecoder::new(&mut archive) {
			Ok(decoder) => {
				tar_check_archive(Archive::new(decoder), tar_str);
				Ok(())
			},
			Err(err) => {
				Err(format!("File {:?} seems to be corrupted, could not decode the zstd contents. Underlying ruzstd error: {}", tar_file, err))
			},
		}
	} else {
		Err(format!(
			"Archive {:?} cannot be analyzed. Only .tar or .tar.xz or .tar.gz or .tar.zst files are supported",
			tar_file
		))
	}
}

fn tar_check_archive<R: Read>(mut archive: Archive<R>, path_str: &str) {
	let mut install_file = String::new();
	let mut all_files = Vec::new();
	let mut executable_files = Vec::new();
	let mut suid_files = Vec::new();
	let archive_files = archive
		.entries()
		.unwrap_or_else(|e| panic!("cannot open archive {}, {}", path_str, e));
	for file in archive_files {
		let mut file =
			file.unwrap_or_else(|e| panic!("cannot access tar file in {}, {}", path_str, e));
		let path = {
			let path = file.header().path().unwrap_or_else(|e| {
				panic!(
					"Failed to extract tar file metadata for file in {}, {}",
					path_str, e,
				)
			});
			path.to_str()
				.unwrap_or_else(|| panic!("{}:{} failed to parse file name", file!(), line!()))
				.to_owned()
		};
		let mode = file.header().mode().unwrap_or_else(|_| {
			panic!(
				"{}:{} Failed to get file mode for file {}",
				file!(),
				line!(),
				path
			)
		});
		let is_normal = !path.ends_with('/') && !path.starts_with('.');
		if is_normal {
			all_files.push(path.clone());
		}
		if is_normal && (mode & 0o111 > 0) {
			executable_files.push(path.clone());
		}
		if mode > 0o777 {
			suid_files.push(path.clone());
		}
		if &path == ".INSTALL" {
			file.read_to_string(&mut install_file).unwrap_or_else(|_| {
				panic!("Failed to read INSTALL script from tar file {}", path_str)
			});
		}
	}

	let has_install = !install_file.is_empty();
	loop {
		if suid_files.is_empty() {
			eprintln!("Package {} has no SUID files.", path_str);
		}
		eprint!("{}=list executable files, ", "[E]".bold());
		eprint!("{}=list all files, ", "[L]".bold());
		eprint!("{}=list files not existing on filesystem, ", "[F]".bold());

		eprint!(
			"{}{}, ",
			"[T]".bold().cyan(),
			"=run shell to inspect".cyan()
		);

		if has_install {
			eprint!(
				"{}=show {}, ",
				"[I]".bold(),
				"install file".bold().bright_red()
			);
		};

		if !suid_files.is_empty() {
			eprint!(
				"{}=list {}, ",
				"[S]".bold(),
				"SUID files".bold().bright_red()
			);
		};
		eprint!("{}=ok, proceed. ", "[O]".bold());
		let string = terminal_util::read_line_lowercase();
		eprintln!();
		if &string == "s" && !suid_files.is_empty() {
			for path in &suid_files {
				eprintln!("{}", path);
			}
		} else if &string == "e" {
			for path in &executable_files {
				eprintln!("{}", path);
			}
		} else if &string == "f" {
			for path in &all_files {
				if !Path::exists(Path::new(&format!("/{}", &path))) {
					eprintln!("{}", path);
				}
			}
		} else if &string == "l" {
			for path in &all_files {
				eprintln!("{}", path);
			}
		} else if &string == "i" && has_install {
			eprintln!("{}", &install_file);
		} else if &string == "t" {
			let dir = PathBuf::from(path_str);
			let dir = dir.parent().unwrap_or_else(|| Path::new("."));
			eprintln!("Exit the shell with `logout` or Ctrl-D...");
			terminal_util::run_env_command(dir, "SHELL", "bash", &[]);
		} else if &string == "o" {
			break;
		} else if &string == "q" {
			eprintln!("Exiting...");
			std::process::exit(-1);
		}
	}
}

pub fn common_suffix_length(pkg_names: &[&str], archive_whitelist: &IndexSet<&str>) -> usize {
	let min_len = pkg_names.iter().map(|p| p.len()).min().unwrap_or(0);
	for suffix_length in 0..min_len {
		for pkg in pkg_names {
			let suffix_start = pkg.len() - suffix_length;
			let prefix = &pkg[..suffix_start];
			if archive_whitelist.contains(prefix) {
				return suffix_length;
			}
		}
	}
	min_len
}

#[cfg(test)]
mod tests {
	use crate::tar_check::*;
	use indexmap::IndexSet;

	fn test(files: &[&str], whitelist: &[&str], expected: usize) {
		let set: IndexSet<&str> = whitelist.iter().copied().collect();
		let result = common_suffix_length(files, &set);
		assert_eq!(result, expected)
	}

	#[test]
	fn test_all() {
		test(&["a-1.pkg.tar", "b-1.pkg.tar"], &["a"], 10);
		test(&["a-1.pkg.tar", "bbbb-1.pkg.tar"], &["a", "dinosaur"], 10);
		test(&["a-x-1.pkg.tar", "b-x-1.pkg.tar"], &["a-x"], 10);
		test(&["a-x-1.pkg.tar", "b-x-1.pkg.tar"], &["a"], 12);
	}
}