clamav/libclamav_rust/src/cdiff.rs
John Humlick dda0a70d90 cdiff: Replace cdiff-apply feature with Rust implementation
Apply both .cdiff and .script CVD patches.

Note: A script is a non-compressed and unsigned file containing cdiff
commands. There is no header or footer that should be processed.

This Rust-based implementation of the cdiff-apply feature includes
equivalent features as found in the C-based implementation:
- cdiff file signature validation against sha256 of the file contents
- Gz decoding of file contents
- File open command
- File close command
- Signature add command
- Line delete command
- Xchg command
- Move command
- Unlink command

This Rust implementation adds cdiff-apply unit tests to verify correct
functionality.
2022-01-10 12:18:33 -07:00

1288 lines
42 KiB
Rust

/*
* Copyright (C) 2021 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*
* Authors: John Humlick
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
extern crate hex;
extern crate openssl;
use std::{
ffi::CString,
fs::{self, File, OpenOptions},
io::{prelude::*, BufReader, Read, Seek, SeekFrom},
iter::*,
os::unix::io::FromRawFd,
process,
};
use flate2::read::GzDecoder;
use log::*;
use openssl::sha;
use thiserror::Error;
const MAX_DSIG_SIZE: usize = 350;
const FILEBUFF: usize = 8192;
const PSS_NSTR: &str = "14783905874077467090262228516557917570254599638376203532031989214105552847269687489771975792123442185817287694951949800908791527542017115600501303394778618535864845235700041590056318230102449612217458549016089313306591388590790796515819654102320725712300822356348724011232654837503241736177907784198700834440681124727060540035754699658105895050096576226753008596881698828185652424901921668758326578462003247906470982092298106789657211905488986281078346361469524484829559560886227198091995498440676639639830463593211386055065360288422394053998134458623712540683294034953818412458362198117811990006021989844180721010947\0";
const PSS_ESTR: &str = "100002053\0";
struct DelNode {
line_no: usize,
del_line: String,
}
struct XchgNode {
line_no: usize,
orig_line: String,
new_line: String,
}
struct Context {
open_db: Option<String>,
add_start: Option<Vec<String>>,
del_start: Option<Vec<DelNode>>,
xchg_start: Option<Vec<XchgNode>>,
}
/// CdiffError enumerates all possible errors returned by this library.
#[derive(Error, Debug)]
pub enum CdiffError {
#[error("No DB open for action {0}")]
NoDBForAction(String),
#[error("File {0} not closed before calling action {1}")]
NotClosedBeforeAction(String, String),
#[error("File {0} not closed before opening {1}")]
NotClosedBeforeOpening(String, String),
#[error("Forbidden characters found in database name {0}")]
ForbiddenCharactersInDB(String),
#[error("Invalid command provided: {0}")]
InvalidCommand(String),
#[error("Unexpected end of line while parsing field: {0}")]
NoMoreData(String),
#[error("File contains fewer than {0} bytes")]
NotEnoughBytes(usize),
#[error("Incorrect file format - {0}")]
MalformedFile(String),
#[error("Move operation failed")]
MoveOpFailed,
#[error("Failed to parse string as a number")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("Cannot perform action {0} on line {1} in file {2}. Pattern does not match.")]
PatternDoesNotMatch(String, usize, String),
#[error("Failed to parse dsig!")]
ParseDsigError,
#[error("Not all delete lines processed at file end")]
NotAllDeleteProcessed,
#[error("Not all exchange lines processed at file end")]
NotAllExchangeProcessed,
/// Represents all other cases of `std::io::Error`.
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error(transparent)]
FromUtf8Error(#[from] std::string::FromUtf8Error),
}
#[derive(Debug)]
pub struct DelOp<'a> {
line_no: usize,
del_line: &'a str,
}
/// Method to parse the cdiff line describing a delete operation
impl<'a> DelOp<'a> {
pub fn new(data: &'a str) -> Result<Self, CdiffError> {
let mut iter = data.split_whitespace();
Ok(DelOp {
line_no: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("line_no".to_string()))?
.parse::<usize>()?,
del_line: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("del_line".to_string()))?,
})
}
}
#[derive(Debug)]
pub struct MoveOp<'a> {
src: &'a str,
dst: &'a str,
start_line_no: usize,
start_line: &'a str,
end_line_no: usize,
end_line: &'a str,
}
/// Method to parse the cdiff line describing a move operation
impl<'a> MoveOp<'a> {
pub fn new(data: &'a str) -> Result<Self, CdiffError> {
let mut iter = data.split_whitespace();
Ok(MoveOp {
src: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("src".to_string()))?,
dst: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("dst".to_string()))?,
start_line_no: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("start_line_no".to_string()))?
.parse::<usize>()?,
start_line: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("start_line".to_string()))?,
end_line_no: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("end_line_no".to_string()))?
.parse::<usize>()?,
end_line: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("end_line".to_string()))?,
})
}
}
#[derive(Debug)]
pub struct XchgOp<'a> {
line_no: usize,
orig_line: &'a str,
new_line: &'a str,
}
/// Method to parse the cdiff line describing an exchange operation
impl<'a> XchgOp<'a> {
pub fn new(data: &'a str) -> Result<Self, CdiffError> {
let mut iter = data.splitn(3, char::is_whitespace);
Ok(XchgOp {
line_no: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("line_no".to_string()))?
.parse::<usize>()?,
orig_line: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("orig_line".to_string()))?,
new_line: iter
.next()
.ok_or_else(|| CdiffError::NoMoreData("new_line".to_string()))?,
})
}
}
extern "C" {
/// int cli_versig2(const unsigned char *sha256, const char *dsig_str, const char *n_str, const char *e_str)
fn cli_versig2(digest: *const u8, dsig: *const i8, n: *const u8, e: *const u8) -> i32;
/// uint8_t cli_get_debug_flag()
fn cli_get_debug_flag() -> u8;
}
fn is_debug_enabled() -> bool {
unsafe {
let debug_flag = cli_get_debug_flag();
match debug_flag {
0 => false,
_ => true,
}
}
}
#[no_mangle]
pub extern "C" fn cdiff_apply(file_descriptor: i32, mode: u16) -> i32 {
debug!(
"cdiff_apply called with file_descriptor={}, mode={}",
file_descriptor, mode
);
let is_cdiff: bool = mode == 1;
let path = std::env::current_dir().unwrap();
debug!("The current directory is {}", path.display());
debug!("Opening file descriptor");
let mut file = unsafe { File::from_raw_fd(file_descriptor) };
debug!("Printing contents of file...");
let mut header_length: usize = 0;
// Only read dsig, header, etc. if this is a cdiff file
if is_cdiff {
let dsig = read_dsig(&mut file);
let dsig = match dsig {
Ok(dsig) => dsig,
Err(e) => {
error!("{:?}", e.to_string());
return -1;
}
};
debug!("Final dsig length is {}", dsig.len());
if is_debug_enabled() {
print_file_data(dsig.clone(), dsig.len() as usize);
}
// Get file length
let file_len = match file.metadata() {
Ok(file_len) => file_len.len() as usize,
Err(e) => {
error!("Failed to get file length: {}", e.to_string());
return -1;
}
};
let footer_offset = file_len - dsig.len() - 1;
// The SHA is calculated from the contents of the beginning of the file
// up until the ':' before the dsig at the end of the file.
let sha256 = get_hash(&mut file, footer_offset);
let sha256 = match sha256 {
Ok(sha256) => sha256,
Err(e) => {
error!("Failed to calculate sha256: {}", e.to_string());
return -1;
}
};
//debug!("sha256 is {} bytes", sha256.len());
debug!("sha256: {}", hex::encode(sha256));
// cli_versig2 will expect dsig to be a null-terminated string
let dsig_cstring = CString::new(dsig);
let dsig_cstring = match dsig_cstring {
Ok(dsig_cstring) => dsig_cstring,
Err(e) => {
error!("Failed to parse dsig: {}", e.to_string());
return -1;
}
};
// Verfify cdiff
let versig_result = unsafe {
cli_versig2(
sha256.to_vec().as_ptr(),
dsig_cstring.as_ptr(),
PSS_NSTR.as_ptr(),
PSS_ESTR.as_ptr(),
)
};
if versig_result != 0 {
error!("cdiff_apply: Incorrect digital signature");
return -1;
}
// Read file length from header
let header_result = read_size(&mut file);
let (header_len, header_offset) = match header_result {
Ok(hl) => hl,
Err(e) => {
error!("{}", e.to_string());
return -1;
}
};
debug!(
"Header len is {}, file len is {}, header offset is {}",
header_len, file_len, header_offset
);
let current_pos = file.seek(SeekFrom::Start(header_offset as u64));
let current_pos = match current_pos {
Ok(current_pos) => current_pos as usize,
Err(e) => {
error!("{}", e.to_string());
return -1;
}
};
debug!("Current file offset is {}", current_pos);
header_length = header_len as usize;
}
// Set reader according to whether this is a script or cdiff
let reader: Box<dyn BufRead> = if is_cdiff {
let gz = GzDecoder::new(file);
Box::new(BufReader::new(gz))
} else {
Box::new(BufReader::new(file))
};
// Create contextual data structure
let mut ctx: Context = Context {
open_db: None,
add_start: None,
del_start: None,
xchg_start: None,
};
match process_lines(&mut ctx, reader, header_length) {
Ok(_) => 0,
Err(e) => {
error!("{}", e.to_string());
-1
}
}
}
/// Set up Context structure with data parsed from command open
fn cmd_open(ctx: &mut Context, db_name: std::string::String) -> Result<(), CdiffError> {
// Test for existing open db
if let Some(x) = &ctx.open_db {
return Err(CdiffError::NotClosedBeforeOpening(x.to_string(), db_name));
}
if !db_name
.chars()
.all(|x: char| x.is_alphanumeric() || x == '\\' || x == '/' || x == '.')
{
return Err(CdiffError::ForbiddenCharactersInDB(db_name));
}
ctx.open_db = Some(db_name);
debug!("data {:?}", ctx.open_db);
Ok(())
}
/// Set up Context structure with data parsed from command add
fn cmd_add(ctx: &mut Context, signature: std::string::String) -> Result<(), CdiffError> {
// Test for add without an open db
match &ctx.open_db {
Some(_x) => (),
_ => return Err(CdiffError::NoDBForAction("ADD".to_string())),
}
// Create a new vector or append to existing one
match &mut ctx.add_start {
Some(add_start) => {
(*add_start).push(signature);
}
_ => {
let add_start = vec![signature];
ctx.add_start = Some(add_start);
}
}
// debug!("signature {:?}", ctx.add_start);
Ok(())
}
/// Set up Context structure with data parsed from command delete
fn cmd_del(mut ctx: &mut Context, del_op: DelOp) -> Result<(), CdiffError> {
// Test for add without an open db
match &ctx.open_db {
Some(_x) => (),
_ => return Err(CdiffError::NoDBForAction("DEL".to_string())),
}
debug!("Deleting {} on line {}", del_op.del_line, del_op.line_no);
// Create a new node for deletion
let del_node = DelNode {
line_no: del_op.line_no,
del_line: del_op.del_line.to_string(),
};
// Create a new vector or append to existing one in order
match &mut ctx.del_start {
Some(del_start) => {
let n = (*del_start).len() - 1;
for i in 0..=n {
// debug!(
// "Deletion: Inserting line_no {} with current line_no {}",
// del_op.line_no, del_start[i].line_no
// );
if del_op.line_no < del_start[i].line_no {
//debug!("Deletion: Inserting into element {}", i);
(*del_start).insert(i, del_node);
break;
} else if i == n {
//debug!("Deletion: Appending to node list");
(*del_start).push(del_node);
break;
}
}
}
_ => {
let del_start = vec![del_node];
//debug!("Deletion: Creating new node list");
ctx.del_start = Some(del_start);
}
}
Ok(())
}
/// Set up Context structure with data parsed from command exchange
fn cmd_xchg(mut ctx: &mut Context, xchg_op: XchgOp) -> Result<(), CdiffError> {
// Test for add without an open db
match &ctx.open_db {
Some(_x) => (),
_ => return Err(CdiffError::NoDBForAction("XCHG".to_string())),
}
debug!(
"Exchanging {} with {} on line {}",
xchg_op.orig_line, xchg_op.new_line, xchg_op.line_no
);
// Create a new node for exchange
let xchg_node = XchgNode {
line_no: xchg_op.line_no,
orig_line: xchg_op.orig_line.to_string(),
new_line: xchg_op.new_line.to_string(),
};
// Create a new vector or append to existing one
match &mut ctx.xchg_start {
Some(xchg_start) => {
(*xchg_start).push(xchg_node);
}
_ => {
let mut xchg_start = Vec::new();
debug!("Exchange: Creating new node list");
xchg_start.push(xchg_node);
ctx.xchg_start = Some(xchg_start);
}
}
Ok(())
}
/// Move range of lines from one DB file into another
fn cmd_move(ctx: &mut Context, move_op: MoveOp) -> Result<(), CdiffError> {
#[derive(PartialEq, Debug)]
enum State {
Init,
Move,
End,
}
let mut state = State::Init;
// Test for move with open db
if let Some(x) = &ctx.open_db {
return Err(CdiffError::NotClosedBeforeAction(
x.to_string(),
"MOVE".to_string(),
));
}
// Open src in read-only mode
let src_file = File::open(move_op.src)?;
// Open dst in append mode
let mut dst_file = OpenOptions::new().append(true).open(move_op.dst)?;
// Create tmp file and open for writing
let tmp_named_file = tempfile::Builder::new()
.prefix("_tmp_move_file")
.tempfile_in("./")?;
let mut tmp_file = tmp_named_file.as_file();
// Create a buffered reader and loop over src, line by line
let src_reader = BufReader::new(src_file);
for (mut line_no, line) in src_reader.lines().enumerate() {
let line = line?;
// cdiff files start at line 1
line_no += 1;
if state == State::Init && line_no == move_op.start_line_no {
if line.starts_with(move_op.start_line) {
state = State::Move;
writeln!(dst_file, "{}", line)?;
} else {
error!("{} does not match {}", line, move_op.start_line);
return Err(CdiffError::PatternDoesNotMatch(
"MOVE".to_string(),
line_no,
move_op.src.to_string(),
));
}
}
// Write everything between start and end to dst
else if state == State::Move {
writeln!(dst_file, "{}", line)?;
if line_no == move_op.end_line_no {
if line.starts_with(move_op.end_line) {
state = State::End;
} else {
return Err(CdiffError::PatternDoesNotMatch(
"MOVE".to_string(),
line_no,
move_op.dst.to_string(),
));
}
}
}
// Write everything outside of start and end to tmp
else {
writeln!(tmp_file, "{}", line)?;
}
}
// Check that we handled start and end
if state != State::End {
return Err(CdiffError::MoveOpFailed);
}
// Delete src and replace it with tmp
fs::remove_file(move_op.src)?;
fs::rename(tmp_named_file.path(), move_op.src)?;
Ok(())
}
/// Utilize Context structure built by various prior command calls to perform I/O on open file
fn cmd_close(mut ctx: &mut Context) -> Result<(), CdiffError> {
// Test for existing open db
match &ctx.open_db {
Some(_x) => (),
_ => return Err(CdiffError::NoDBForAction("CLOSE".to_string())),
}
let open_db = ctx.open_db.as_ref().unwrap();
debug!("Close DB {}", open_db);
let mut delete_lines: bool = false;
let mut xchg_lines: bool = false;
if ctx.del_start.is_some() {
delete_lines = true;
debug!("Found lines to delete");
}
if ctx.xchg_start.is_some() {
xchg_lines = true;
}
if delete_lines || xchg_lines {
// Open src in read-only mode
let src_file = File::open(open_db)?;
// Create a buffered reader and loop over src, line by line
let src_reader = BufReader::new(src_file);
// Create tmp file and open for writing
let tmp_named_file = tempfile::Builder::new()
.prefix("_tmp_move_file")
.tempfile_in("./")?;
let mut tmp_file = tmp_named_file.as_file();
let mut cur_del_node: usize = 0;
let mut cur_xchg_node: usize = 0;
let mut del_vec: Vec<DelNode> = vec![];
let mut xchg_vec: Vec<XchgNode> = vec![];
// Test for lines to delete
let mut del_vec_len: usize = 0;
if let Some(del_vec_ref) = &mut ctx.del_start {
del_vec = std::mem::take(del_vec_ref);
del_vec_len = del_vec.len();
}
// Test for lines to exchange
let mut xchg_vec_len: usize = 0;
if let Some(xchg_vec_ref) = &mut ctx.xchg_start {
xchg_vec = std::mem::take(xchg_vec_ref);
xchg_vec_len = xchg_vec.len();
}
for (mut line_no, line) in src_reader.lines().enumerate() {
let line = line?;
// cdiff lines start at 1
line_no += 1;
// if delete_lines {
// debug!("First element in delete list: cur_line == {} line_no == {} del_line = {}",
// line_no, del_vec[0].line_no, del_vec[0].del_line);
// debug!("is_empty {}, cur_del_node {}, del_vec_len {}",
// !del_vec.is_empty(), cur_del_node, del_vec_len);
// }
if delete_lines
&& !del_vec.is_empty()
&& cur_del_node < del_vec_len
&& line_no == del_vec[cur_del_node].line_no
{
let del_line = &del_vec[cur_del_node].del_line;
debug!(
"deleting line on line_no starting with: {} {} {}",
line, line_no, del_line
);
// Make sure that the line we are deleting matches what is expected
if !line.starts_with(del_line.as_str()) {
return Err(CdiffError::PatternDoesNotMatch(
"delete".to_string(),
line_no,
open_db.to_string(),
));
}
// Increment del node
cur_del_node += 1;
if cur_del_node > del_vec_len {
del_vec_len = 0;
}
// Do nothing - Do not write this line to file
} else if xchg_lines
&& !xchg_vec.is_empty()
&& cur_xchg_node < xchg_vec_len
&& line_no == xchg_vec[cur_xchg_node].line_no
{
let orig_line = &xchg_vec[cur_xchg_node].orig_line;
let new_line = &xchg_vec[cur_xchg_node].new_line;
debug!("Comparing line with orig_line: {} == {}", line, orig_line);
if !line.starts_with(orig_line.as_str()) {
return Err(CdiffError::PatternDoesNotMatch(
"exchange".to_string(),
line_no,
open_db.to_string(),
));
}
// Write exchange line to file
writeln!(tmp_file, "{}", new_line)?;
// Increment xchange node
cur_xchg_node += 1;
if cur_xchg_node > xchg_vec_len {
xchg_vec_len = 0;
}
}
// Write the line as is
else {
writeln!(tmp_file, "{}", line)?;
}
}
// Make sure that all delete and exchange lines were processed
if delete_lines && cur_del_node < del_vec.len() {
return Err(CdiffError::NotAllDeleteProcessed);
}
if xchg_lines && cur_xchg_node < xchg_vec.len() {
return Err(CdiffError::NotAllExchangeProcessed);
}
// Delete the old file and replace it with tmp
fs::remove_file(open_db.clone())?;
fs::rename(tmp_named_file.path(), open_db.clone())?;
}
// Test for lines to add
if let Some(add_start) = &ctx.add_start {
let mut db_file = OpenOptions::new().append(true).open(open_db.clone())?;
for sig in add_start {
debug!("Writing signature {} to file {}", sig, open_db);
writeln!(db_file, "{}", sig)?;
}
ctx.add_start = None;
}
ctx.open_db = None;
ctx.del_start = None;
ctx.xchg_start = None;
debug!("Close finished");
Ok(())
}
/// Set up Context structure with data parsed from command unlink
fn cmd_unlink(ctx: &mut Context) -> Result<(), CdiffError> {
match &ctx.open_db {
Some(open_db) => fs::remove_file(open_db.clone())?,
_ => return Err(CdiffError::NoDBForAction("UNLINK".to_string())),
}
Ok(())
}
/// Handle a specific command line in a cdiff file, calling the appropriate handler function
fn process_line(ctx: &mut Context, line: String) -> Result<(), CdiffError> {
// Find the index at the end of the command (note that the CLOSE command has no trailing data)
let spc_idx = match line.find(|c: char| c.is_whitespace()) {
Some(spc_idx) => spc_idx,
None => match line == "CLOSE" {
true => 0,
_ => {
error!("Unable to parse cmd");
process::abort();
}
},
};
// Get the command
let cmd: String = if spc_idx > 0 {
line.chars().take(spc_idx).collect()
} else {
line.clone()
};
// Get the data and clean it up
let data: String = line.chars().skip(spc_idx + 1).collect::<String>();
let data: String = data.trim().to_owned();
debug!("cmd = {}", cmd);
// Call the appropriate command function
match cmd.as_str() {
"OPEN" => cmd_open(ctx, data),
"ADD" => cmd_add(ctx, data),
"DEL" => {
let del_op = DelOp::new(data.as_str())?;
cmd_del(ctx, del_op)
}
"XCHG" => {
let xchg_op = XchgOp::new(data.as_str())?;
cmd_xchg(ctx, xchg_op)
}
"CLOSE" => cmd_close(ctx),
"MOVE" => {
let move_op = MoveOp::new(data.as_str())?;
cmd_move(ctx, move_op)
}
"UNLINK" => cmd_unlink(ctx),
_ => Err(CdiffError::InvalidCommand(cmd.to_string())),
}
}
/// Main loop for iterating over cdiff command lines and handling them
fn process_lines<T>(
ctx: &mut Context,
reader: T,
uncompressed_size: usize,
) -> Result<(), CdiffError>
where
T: BufRead,
{
let mut decompressed_bytes = 0;
let mut n = 0;
for line in reader.lines() {
match line {
Ok(line) => {
// Line buffer resize commands are a vestige from cdiff.c
if line.starts_with('#') {
debug!("Buffer resize detected in line {}", line);
continue;
}
n += 1;
decompressed_bytes = decompressed_bytes + line.len() + 1;
debug!("Line {}: {:?}", n, line);
process_line(ctx, line)?;
}
Err(e) => {
return Err(CdiffError::MalformedFile(e.to_string()));
}
}
}
debug!(
"Expected {} decompressed bytes, read {} decompressed bytes",
uncompressed_size, decompressed_bytes
);
Ok(())
}
/// Find the signature at the end of the file, prefixed by ':'
fn read_dsig(file: &mut File) -> Result<Vec<u8>, CdiffError> {
// Verify file length
if file.metadata()?.len() < MAX_DSIG_SIZE as u64 {
return Err(CdiffError::NotEnoughBytes(MAX_DSIG_SIZE));
}
// Seek to the dsig_offset
file.seek(SeekFrom::End(-(MAX_DSIG_SIZE as i64)))?;
// Read from dsig_offset to EOF
let mut dsig: Vec<u8> = vec![];
file.read_to_end(&mut dsig)?;
debug!("dsig length is {}", dsig.len());
// Find the signature
let offset: usize = MAX_DSIG_SIZE + 1;
// Read in reverse until the delimiter ':' is found
// let offset = dsig.iter().enumerate().rev().find(|(i, value)| **value == b':');
if let Some(dsig) = dsig.rsplit(|v| *v == b':').next() {
if dsig.len() > MAX_DSIG_SIZE {
Err(CdiffError::ParseDsigError)
} else {
Ok(dsig.to_vec())
}
} else {
Ok(dsig[offset..].to_vec())
}
}
// Returns the parsed, uncompressed file size from the header, as well
// as the offset in the file that the header ends.
fn read_size(file: &mut File) -> Result<(u32, usize), CdiffError> {
// Seek to beginning of file.
file.seek(SeekFrom::Start(0))?;
// File should always start with "ClamAV-Diff".
let prefix = b"ClamAV-Diff";
let mut buf = Vec::with_capacity(prefix.len());
file.take(prefix.len() as u64).read_to_end(&mut buf)?;
if buf.as_slice() != prefix.to_vec().as_slice() {
return Err(CdiffError::MalformedFile("malformed header".to_string()));
}
// Read up to FILEBUFF to parse out the file size.
let n = file.take(FILEBUFF as u64).read_to_end(&mut buf)?;
let mut colons = 0;
let mut file_size_vec = Vec::new();
debug!("n == {}", n);
//for i in 0..=n {
for (i, value) in buf.iter().enumerate().take(n + 1) {
// Colon found, increment count.
if *value == b':' {
colons += 1;
}
// We are reading the file size now.
else if colons == 2 {
file_size_vec.push(*value);
}
// We are done reading the file size.
if colons == 3 {
let file_size_str = String::from_utf8(file_size_vec)?;
debug!("file_size_str == {}", file_size_str);
return Ok((file_size_str.parse::<u32>()?, i + 1));
}
}
Err(CdiffError::MalformedFile("insufficient colons".to_string()))
}
/// Calculate the sha256 of the first len bytes of a file
fn get_hash(file: &mut File, len: usize) -> Result<[u8; 32], CdiffError> {
let mut hasher = sha::Sha256::new();
// Seek to beginning of file
file.seek(SeekFrom::Start(0))?;
let mut sum: usize = 0;
// Read FILEBUFF (8192) bytes at a time,
// calculating the hash along the way. Stop
// after signature is reached.
loop {
let mut buf = Vec::with_capacity(FILEBUFF);
let n = file.take(FILEBUFF as u64).read_to_end(&mut buf)?;
if sum + n >= len {
// update with len - sum
hasher.update(&buf[..(len - sum)]);
//print_file_data(buf, len - sum);
let hash = hasher.finish();
return Ok(hash);
} else {
// update with n
hasher.update(&buf);
//print_file_data(buf, n);
}
sum += n;
}
}
fn print_file_data(buf: Vec<u8>, len: usize) {
for (i, value) in buf.iter().enumerate().take(len) {
eprint!("{:#02X} ", value);
if (i + 1) % 16 == 0 {
eprint!("");
}
}
eprint!("\n");
}
#[cfg(test)]
mod tests {
use super::*;
/// CdiffTestError enumerates all possible errors returned by this testing library.
#[derive(Error, Debug)]
pub enum CdiffTestError {
/// Represents all other cases of `std::io::Error`.
#[error(transparent)]
IOError(#[from] std::io::Error),
#[error(transparent)]
FromUtf8Error(#[from] std::string::FromUtf8Error),
}
#[test]
fn parse_move_works() {
println!("MOVE was called!");
let move_op = MoveOp::new("a b 1 hello 2 world").expect("Should've worked!");
println!("{:?}", move_op);
assert_eq!(move_op.src, "a");
assert_eq!(move_op.dst, "b");
assert_eq!(move_op.start_line_no, 1);
assert_eq!(move_op.start_line, "hello");
assert_eq!(move_op.end_line_no, 2);
assert_eq!(move_op.end_line, "world");
}
#[test]
fn parse_move_fail_int() {
println!("MOVE was called!");
let err = MoveOp::new("a b NOTANUMBER hello 2 world").expect_err("Should've failed!");
let parse_string = "NOTANUMBER".to_string();
let parse_error = parse_string.parse::<usize>().unwrap_err();
let number_error = CdiffError::ParseIntError(parse_error);
println!("{:?}", err.to_string());
assert_eq!(err.to_string(), number_error.to_string());
}
#[test]
fn parse_move_fail_eof() {
println!("MOVE was called!");
let err = MoveOp::new("a b 1").expect_err("Should've failed!");
let start_line_err = CdiffError::NoMoreData("start_line".to_string());
println!("{:?}", err.to_string());
assert_eq!(err.to_string(), start_line_err.to_string());
}
/// Helper function to set up a test folder and initialize a pseudo-db file with specified data.
fn initialize_db_file_with_data(
initial_data: Vec<&str>,
) -> Result<tempfile::TempPath, CdiffTestError> {
let mut file = tempfile::Builder::new()
.tempfile_in("./")
.expect("Failed to create temp file");
for line in initial_data {
writeln!(file, "{}", line).expect("Failed to write line to temp file");
}
Ok(file.into_temp_path())
}
/// Compare provided vector data with file contents
fn compare_file_with_expected(
temp_file_path: tempfile::TempPath,
expected_data: &mut Vec<&str>,
) {
let db_file = File::open(temp_file_path).unwrap();
let reader = BufReader::new(db_file);
// We will be popping lines off a stack, so we need to reverse the vec
expected_data.reverse();
for (index, line) in reader.lines().enumerate() {
let expected_line = expected_data
.pop()
.expect("Expected data ran out before file!");
assert_eq!(
expected_line,
line.expect("Failed to read line from temp file")
);
debug!(
"Data \"{}\" matches expected result on line {}",
expected_line, index
);
}
// expected_data should be empty here
assert_eq!(expected_data.len(), 0);
}
fn construct_ctx_from_path(path: &tempfile::TempPath) -> Context {
let ctx: Context = Context {
open_db: Some(path.to_str().unwrap().to_string()),
add_start: None,
del_start: None,
xchg_start: None,
};
ctx
}
#[test]
fn delete_first_line() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["AAAA", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let del_op = DelOp::new("1 ClamAV-VDB:14").unwrap();
cmd_del(&mut ctx, del_op).expect("cmd_del failed");
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn delete_second_line() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let del_op = DelOp::new("2 AAAA").unwrap();
cmd_del(&mut ctx, del_op).expect("cmd_del failed");
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn delete_last_line() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let del_op = DelOp::new("4 CCCC").unwrap();
cmd_del(&mut ctx, del_op).expect("cmd_del failed");
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn delete_line_not_match() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let del_op = DelOp::new("1 CCCC").unwrap();
cmd_del(&mut ctx, del_op).expect("cmd_del failed");
match cmd_close(&mut ctx) {
Ok(_) => panic!("cmd_close should have failed!"),
Err(e) => {
assert!(e
.to_string()
.starts_with("Cannot perform action delete on line"));
}
}
}
#[test]
fn delete_out_of_bounds() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let del_op = DelOp::new("5 CCCC").unwrap();
cmd_del(&mut ctx, del_op).expect("cmd_del failed");
match cmd_close(&mut ctx) {
Ok(_) => panic!("cmd_close should have failed!"),
Err(e) => {
assert_eq!(e.to_string(), "Not all delete lines processed at file end");
}
}
}
#[test]
fn exchange_first_line() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["ClamAV-VDB:15 Aug 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let xchg_op = XchgOp::new("1 ClamAV-VDB:14 ClamAV-VDB:15 Aug 2021 14-29 -0400").unwrap();
cmd_xchg(&mut ctx, xchg_op).expect("cmd_xchg failed");
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn exchange_second_line() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "DDDD", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let xchg_op = XchgOp::new("2 AAAA DDDD").unwrap();
cmd_xchg(&mut ctx, xchg_op).expect("cmd_xchg failed");
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn exchange_last_line() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "DDDD"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let xchg_op = XchgOp::new("4 CCCC DDDD").unwrap();
cmd_xchg(&mut ctx, xchg_op).expect("cmd_xchg failed");
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn exchange_out_of_bounds() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
let xchg_op = XchgOp::new("5 DDDD EEEE").unwrap();
cmd_xchg(&mut ctx, xchg_op).expect("cmd_xchg failed");
match cmd_close(&mut ctx) {
Ok(_) => panic!("cmd_close should have failed!"),
Err(e) => {
assert_eq!(
e.to_string(),
"Not all exchange lines processed at file end"
);
}
}
}
#[test]
fn add_delete_exchange() {
let initial_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "AAAA", "BBBB", "CCCC"];
let mut expected_data = vec!["ClamAV-VDB:14 Jul 2021 14-29 -0400", "DDDD", "CCCC", "EEEE"];
let db_file_path = initialize_db_file_with_data(initial_data).unwrap();
// Create contextual data structure with open_db path
let mut ctx = construct_ctx_from_path(&db_file_path);
// Add a line
cmd_add(&mut ctx, "EEEE".to_string()).unwrap();
// Delete the 2nd line
let del_op = DelOp::new("2 AAAA").unwrap();
cmd_del(&mut ctx, del_op).expect("cmd_del failed");
// Exchange the 3rd line
let xchg_op = XchgOp::new("3 BBBB DDDD").unwrap();
cmd_xchg(&mut ctx, xchg_op).expect("cmd_xchg failed");
// Perform all operations and close the file
match cmd_close(&mut ctx) {
Ok(_) => (),
Err(e) => panic!("cmd_close failed with: {}", e.to_string()),
}
compare_file_with_expected(db_file_path, &mut expected_data);
}
#[test]
fn move_db() {
// Define initial databases
let dst_data = vec!["ClamAV-VDB:15 Aug 2021 14-30 -0400", "AAAA", "BBBB", "CCCC"];
let src_data = vec![
"ClamAV-VDB:14 Jul 2021 14-29 -0400",
"AAAA",
"BBBB",
"CCCC",
"DDDD",
"EEEE",
"FFFF",
"GGGG",
];
// Define expected databases after move operation
let mut expected_dst_data = vec![
"ClamAV-VDB:15 Aug 2021 14-30 -0400",
"AAAA",
"BBBB",
"CCCC",
"DDDD",
"EEEE",
"FFFF",
];
let mut expected_src_data = vec![
"ClamAV-VDB:14 Jul 2021 14-29 -0400",
"AAAA",
"BBBB",
"CCCC",
"GGGG",
];
// Initialize databases
let dst_file_path = initialize_db_file_with_data(dst_data).unwrap();
let src_file_path = initialize_db_file_with_data(src_data).unwrap();
let mut ctx: Context = Context {
open_db: None,
add_start: None,
del_start: None,
xchg_start: None,
};
let move_args = format!(
"{} {} 5 DDDD 7 FFFF",
src_file_path.to_str().unwrap(),
dst_file_path.to_str().unwrap()
);
// Move lines 5-7 from src to dst
let move_op = MoveOp::new(move_args.as_str()).unwrap();
match cmd_move(&mut ctx, move_op) {
Ok(_) => (),
Err(e) => panic!("cmd_move failed with: {}", e.to_string()),
}
compare_file_with_expected(src_file_path, &mut expected_src_data);
compare_file_with_expected(dst_file_path, &mut expected_dst_data);
}
}