Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for extracting attachments from OneNote section files
Browse files Browse the repository at this point in the history
Includes rudimentary support for getting slices from FMap's and for
interacting with libclamav's context structure.

For now will use a Cisco-Talos org fork of the onenote_parser
until the feature to read open a onenote section from a slice (instead
of from a filepath) is added to the upstream.
val-ms committed Nov 17, 2023
1 parent 0f9de9e commit 27a9c40
Showing 22 changed files with 912 additions and 349 deletions.
402 changes: 149 additions & 253 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions libclamav/filetypes.c
Original file line number Diff line number Diff line change
@@ -138,6 +138,7 @@ static const struct ftmap_s {
{ "CL_TYPE_EGG", CL_TYPE_EGG },
{ "CL_TYPE_EGGSFX", CL_TYPE_EGGSFX },
{ "CL_TYPE_UDF", CL_TYPE_UDF },
{ "CL_TYPE_ONENOTE", CL_TYPE_ONENOTE },
{ NULL, CL_TYPE_IGNORED }
};
// clang-format on
1 change: 1 addition & 0 deletions libclamav/filetypes.h
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ typedef enum cli_file {
CL_TYPE_OOXML_HWP,
CL_TYPE_PS,
CL_TYPE_EGG,
CL_TYPE_ONENOTE,

/* Section for partition types */
CL_TYPE_PART_ANY, /* unknown partition type */
1 change: 1 addition & 0 deletions libclamav/filetypes_int.h
Original file line number Diff line number Diff line change
@@ -204,5 +204,6 @@ static const char *ftypes_int[] = {
"0:0:4d4d:TIFF Big Endian:CL_TYPE_ANY:CL_TYPE_GRAPHICS:81:121",
"1:*:377abcaf271c:7zip-SFX:CL_TYPE_ANY:CL_TYPE_7ZSFX:74",
"1:0:3c3f786d6c2076657273696f6e3d22312e3022{0-1024}70726f6769643d22576f72642e446f63756d656e74223f3e:Microsoft Word 2003 XML Document:CL_TYPE_ANY:CL_TYPE_XML_WORD:80",
"0:0:e4525c7b8cd8a74daeb15378d02996d3:Microsoft OneNote Document:CL_TYPE_ANY:CL_TYPE_ONENOTE:200",
NULL};
#endif
1 change: 1 addition & 0 deletions libclamav/libclamav.map
Original file line number Diff line number Diff line change
@@ -297,6 +297,7 @@ CLAMAV_PRIVATE {
readdb_parse_ldb_subsignature;
fuzzy_hash_calculate_image;
ffierror_fmt;
cli_magic_scan_buff;

__cli_strcasestr;
__cli_strndup;
1 change: 1 addition & 0 deletions libclamav/others.h
Original file line number Diff line number Diff line change
@@ -191,6 +191,7 @@ typedef struct recursion_level_tag {
} recursion_level_t;

typedef void *evidence_t;
typedef void *onedump_t;

/* internal clamav context */
typedef struct cli_ctx_tag {
5 changes: 5 additions & 0 deletions libclamav/scanners.c
Original file line number Diff line number Diff line change
@@ -4591,6 +4591,11 @@ cl_error_t cli_magic_scan(cli_ctx *ctx, cli_file_t type)
ret = cli_scanegg(ctx);
break;

case CL_TYPE_ONENOTE:
if (SCAN_PARSE_ARCHIVE && (DCONF_ARCH & ARCH_CONF_EGG))
ret = scan_onenote(ctx);
break;

case CL_TYPE_OOXML_WORD:
case CL_TYPE_OOXML_PPT:
case CL_TYPE_OOXML_XL:
2 changes: 2 additions & 0 deletions libclamav_rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@ base64 = "0.21.0"
sha1 = "0.10.5"
unicode-segmentation = "1.10.1"
bindgen = "0.65"
onenote_parser = { git = "https://github.com/Cisco-Talos/onenote.rs.git", branch = "CLAM-2329-new-from-slice" }
hex-literal = "0.4.1"

[lib]
crate-type = ["staticlib"]
6 changes: 5 additions & 1 deletion libclamav_rust/build.rs
Original file line number Diff line number Diff line change
@@ -52,6 +52,7 @@ const BINDGEN_FUNCTIONS: &[&str] = &[
"cli_versig2",
"cli_getdsig",
"cli_get_debug_flag",
"cli_magic_scan_buff",
];

// Generate bindings for these types (structs, enums):
@@ -61,6 +62,7 @@ const BINDGEN_TYPES: &[&str] = &[
"cli_ac_result",
"css_image_extractor_t",
"css_image_handle_t",
"onedump_t",
];

// Find the required functions and types in these headers:
@@ -70,6 +72,8 @@ const BINDGEN_HEADERS: &[&str] = &[
"../libclamav/others.h",
"../libclamav/dsig.h",
"../libclamav/htmlnorm.h",
"../libclamav/fmap.h",
"../libclamav/scanners.h",
];

// Find the required headers in these directories:
@@ -135,7 +139,7 @@ fn execute_bindgen() -> Result<(), &'static str> {
// Silence code-style warnings for generated bindings.
.raw_line("#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]")
// Make the bindings pretty.
.rustfmt_bindings(true)
.formatter(bindgen::Formatter::Rustfmt)
// Disable the layout tests.
// We're commiting to source control. Pointer width, integer size, etc
// are probably not the same when generated as when compiled.
1 change: 1 addition & 0 deletions libclamav_rust/cbindgen.toml
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ include = [
"evidence::evidence_num_indicators_type",
"evidence::evidence_add_indicator",
"evidence::IndicatorType",
"scanners::scan_onenote",
]

# prefix = "CAPI_"
55 changes: 27 additions & 28 deletions libclamav_rust/src/cdiff.rs
Original file line number Diff line number Diff line change
@@ -36,7 +36,6 @@ use crate::validate_str_param;
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use log::{debug, error, warn};
use sha2::{Digest, Sha256};
use thiserror::Error;

/// Size of a digital signature
const SIG_SIZE: usize = 350;
@@ -88,8 +87,8 @@ struct Context {
}

/// Possible errors returned by cdiff_apply() and script2cdiff
#[derive(Debug, Error)]
pub enum CdiffError {
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Error in header: {0}")]
Header(#[from] HeaderError),

@@ -151,7 +150,7 @@ pub enum CdiffError {

/// Errors particular to input handling (i.e., syntax, or side effects from
/// handling input)
#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum InputError {
#[error("Unsupported command provided: {0}")]
UnknownCommand(String),
@@ -199,7 +198,7 @@ pub enum InputError {
}

/// Errors encountered while processing
#[derive(Debug, Error)]
#[derive(thiserror::Error, Debug)]
pub enum ProcessingError {
#[error("File {0} not closed before calling action MOVE")]
NotClosedBeforeAction(String),
@@ -238,7 +237,7 @@ pub enum ProcessingError {
IoError(#[from] std::io::Error),
}

#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum HeaderError {
#[error("invalid magic")]
BadMagic,
@@ -253,7 +252,7 @@ pub enum HeaderError {
IoError(#[from] std::io::Error),
}

#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum SignatureError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
@@ -265,7 +264,7 @@ pub enum SignatureError {
TooLarge,
}

#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum InvalidNumber {
#[error("not unicode")]
NotUnicode(#[from] std::str::Utf8Error),
@@ -462,7 +461,7 @@ pub extern "C" fn _script2cdiff(
/// signature from the sha256 of the contents written.
///
/// This function will panic if any of the &str parameters contain interior NUL bytes
pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Result<(), CdiffError> {
pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Result<(), Error> {
// Make a copy of the script file name to use for the cdiff file
let cdiff_file_name_string = script_file_name.to_string();
let mut cdiff_file_name = cdiff_file_name_string.as_str();
@@ -476,40 +475,40 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
// Get right-most hyphen index
let hyphen_index = cdiff_file_name
.rfind('-')
.ok_or(CdiffError::FilenameMissingHyphen)?;
.ok_or(Error::FilenameMissingHyphen)?;

// Get the version, which should be to the right of the hyphen
let version_string = cdiff_file_name
.get((hyphen_index + 1)..)
.ok_or(CdiffError::FilenameMissingVersion)?;
.ok_or(Error::FilenameMissingVersion)?;

// Parse the version into usize
let version = version_string
.to_string()
.parse::<usize>()
.map_err(CdiffError::VersionParse)?;
.map_err(Error::VersionParse)?;

// Add .cdiff suffix
let cdiff_file_name = format!("{}.{}", cdiff_file_name, "cdiff");
debug!("script2cdiff() - writing to: {:?}", &cdiff_file_name);

// Open cdiff_file_name for writing
let mut cdiff_file: File = File::create(&cdiff_file_name)
.map_err(|e| CdiffError::FileCreate(cdiff_file_name.to_owned(), e))?;
.map_err(|e| Error::FileCreate(cdiff_file_name.to_owned(), e))?;

// Open the original script file for reading
let script_file: File = File::open(script_file_name)
.map_err(|e| CdiffError::FileOpen(script_file_name.to_owned(), e))?;
.map_err(|e| Error::FileOpen(script_file_name.to_owned(), e))?;

// Get file length
let script_file_len = script_file
.metadata()
.map_err(|e| CdiffError::FileMeta(script_file_name.to_owned(), e))?
.map_err(|e| Error::FileMeta(script_file_name.to_owned(), e))?
.len();

// Write header to cdiff file
write!(cdiff_file, "ClamAV-Diff:{}:{}:", version, script_file_len)
.map_err(|e| CdiffError::FileWrite(script_file_name.to_owned(), e))?;
.map_err(|e| Error::FileWrite(script_file_name.to_owned(), e))?;

// Set up buffered reader and gz writer
let mut reader = BufReader::new(script_file);
@@ -521,12 +520,12 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
// Get cdiff file writer back from flate2
let mut cdiff_file = gz
.finish()
.map_err(|e| CdiffError::FileWrite(cdiff_file_name.to_owned(), e))?;
.map_err(|e| Error::FileWrite(cdiff_file_name.to_owned(), e))?;

// Get the new cdiff file len
let cdiff_file_len = cdiff_file
.metadata()
.map_err(|e| CdiffError::FileMeta(cdiff_file_name.to_owned(), e))?
.map_err(|e| Error::FileMeta(cdiff_file_name.to_owned(), e))?
.len();
debug!(
"script2cdiff() - wrote {} bytes to {}",
@@ -536,7 +535,7 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
// Calculate SHA2-256 to get the sigature
// TODO: Do this while the file is being written
let bytes = std::fs::read(&cdiff_file_name)
.map_err(|e| CdiffError::FileRead(cdiff_file_name.to_owned(), e))?;
.map_err(|e| Error::FileRead(cdiff_file_name.to_owned(), e))?;
let sha256 = {
let mut hasher = Sha256::new();
hasher.update(&bytes);
@@ -561,12 +560,12 @@ pub fn script2cdiff(script_file_name: &str, builder: &str, server: &str) -> Resu
// Write cdiff footer delimiter
cdiff_file
.write_all(b":")
.map_err(|e| CdiffError::FileWrite(cdiff_file_name.to_owned(), e))?;
.map_err(|e| Error::FileWrite(cdiff_file_name.to_owned(), e))?;

// Write dsig to cdiff footer
cdiff_file
.write_all(dsig.to_bytes())
.map_err(|e| CdiffError::FileWrite(cdiff_file_name, e))?;
.map_err(|e| Error::FileWrite(cdiff_file_name, e))?;

// Exit success
Ok(())
@@ -609,7 +608,7 @@ pub extern "C" fn _cdiff_apply(fd: i32, mode: u16) -> i32 {
/// A cdiff file contains a footer that is the signed signature of the sha256
/// file contains of the header and the body. The footer begins after the first
/// ':' character to the left of EOF.
pub fn cdiff_apply(file: &mut File, mode: ApplyMode) -> Result<(), CdiffError> {
pub fn cdiff_apply(file: &mut File, mode: ApplyMode) -> Result<(), Error> {
let path = std::env::current_dir().unwrap();
debug!("cdiff_apply() - current directory is {}", path.display());

@@ -649,7 +648,7 @@ pub fn cdiff_apply(file: &mut File, mode: ApplyMode) -> Result<(), CdiffError> {
};
debug!("cdiff_apply() - cli_versig2() result = {}", versig_result);
if versig_result != 0 {
return Err(CdiffError::InvalidDigitalSignature);
return Err(Error::InvalidDigitalSignature);
}

// Read file length from header
@@ -1042,7 +1041,7 @@ fn process_lines<T>(
ctx: &mut Context,
reader: &mut T,
uncompressed_size: usize,
) -> Result<(), CdiffError>
) -> Result<(), Error>
where
T: BufRead,
{
@@ -1059,7 +1058,7 @@ where
match linebuf.first() {
// Skip comment lines
Some(b'#') => continue,
_ => process_line(ctx, &linebuf).map_err(|e| CdiffError::Input {
_ => process_line(ctx, &linebuf).map_err(|e| Error::Input {
line: line_no,
err: e,
operation: String::from_utf8_lossy(&linebuf).to_string(),
@@ -1150,7 +1149,7 @@ fn read_size(file: &mut File) -> Result<(u32, usize), HeaderError> {
}

/// Calculate the sha256 of the first len bytes of a file
fn get_hash(file: &mut File, len: usize) -> Result<[u8; 32], CdiffError> {
fn get_hash(file: &mut File, len: usize) -> Result<[u8; 32], Error> {
let mut hasher = Sha256::new();

// Seek to beginning of file
@@ -1193,7 +1192,7 @@ mod tests {
use std::path::Path;

/// CdiffTestError enumerates all possible errors returned by this testing library.
#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum CdiffTestError {
/// Represents all other cases of `std::io::Error`.
#[error(transparent)]
@@ -1545,7 +1544,7 @@ mod tests {
fn script2cdiff_missing_hyphen() {
assert!(matches!(
script2cdiff("", "", ""),
Err(CdiffError::FilenameMissingHyphen)
Err(Error::FilenameMissingHyphen)
));
}
}
11 changes: 5 additions & 6 deletions libclamav_rust/src/css_image_extract.rs
Original file line number Diff line number Diff line change
@@ -24,14 +24,13 @@ use std::{ffi::CStr, mem::ManuallyDrop, os::raw::c_char};

use base64::{engine::general_purpose as base64_engine_standard, Engine as _};
use log::{debug, error, warn};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;

use crate::sys;

/// CdiffError enumerates all possible errors returned by this library.
#[derive(Error, Debug)]
pub enum CssExtractError {
/// Error enumerates all possible errors returned by this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid format")]
Format,

@@ -56,7 +55,7 @@ pub struct CssImageExtractor<'a> {
}

impl<'a> CssImageExtractor<'a> {
pub fn new(css: &'a str) -> Result<Self, CssExtractError> {
pub fn new(css: &'a str) -> Result<Self, Error> {
Ok(Self { remaining: css })
}

@@ -152,7 +151,7 @@ impl<'a> CssImageExtractor<'a> {
};

// Trim off " at end.
let c = url_parameter.graphemes(true).rev().next();
let c = url_parameter.graphemes(true).next_back();
if let Some(c) = c {
if c == "\"" {
(url_parameter, _) = url_parameter.split_at(url_parameter.len() - 1);
98 changes: 98 additions & 0 deletions libclamav_rust/src/ctx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Rust equivalent of libclamav's scanners.c module
*
* Copyright (C) 2023 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*
* Authors: Micah Snyder
*
* 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.
*/

use std::{convert::TryInto, path::PathBuf, slice};

use crate::{fmap::FMap, sys::cli_ctx, util::str_from_ptr};

/// Error enumerates all possible errors returned by this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid format")]
Format,

#[error("Invalid NULL pointer: {0}")]
NullPointer(&'static str),

#[error("{0} parameter is NULL")]
NullParam(&'static str),

#[error("No more files to extract")]
NoMoreFiles,

#[error("Invalid FMap: {0}")]
BadMap(#[from] crate::fmap::Error),

#[error("String not UTF8: {0}")]
Utf8(#[from] std::str::Utf8Error),
}

/// Get the ctx.sub_filepath as an Option<'str>
///
/// # Safety
///
/// Must be a valid ctx pointer.
pub unsafe fn sub_filepath(ctx: *mut cli_ctx) -> Result<Option<PathBuf>, Error> {
if ctx.is_null() {
return Err(Error::NullPointer("ctx"));
}

Ok(str_from_ptr(unsafe { *ctx }.sub_filepath)
.map_err(Error::Utf8)?
.map(PathBuf::from))
}

/// Get the ctx.target_filepath as an Option<'str>
///
/// # Safety
///
/// Must be a valid ctx pointer.
pub unsafe fn target_filepath(ctx: *mut cli_ctx) -> Result<Option<PathBuf>, Error> {
if ctx.is_null() {
return Err(Error::NullPointer("ctx"));
}

Ok(str_from_ptr(unsafe { *ctx }.target_filepath)
.map_err(Error::Utf8)?
.map(PathBuf::from))
}

/// Get the fmap for the current layer.
///
/// # Safety
///
/// Must be a valid ctx pointer.
pub unsafe fn current_fmap(ctx: *mut cli_ctx) -> Result<FMap, Error> {
if ctx.is_null() {
return Err(Error::NullPointer("ctx"));
}

let recursion_stack_size = unsafe { *ctx }.recursion_stack_size as usize;
let recursion_level = unsafe { *ctx }.recursion_level as usize;

let recursion_stack =
unsafe { slice::from_raw_parts((*ctx).recursion_stack, recursion_stack_size) };

let current_level = recursion_stack[recursion_level];

current_level.fmap.try_into().map_err(Error::BadMap)
}
15 changes: 7 additions & 8 deletions libclamav_rust/src/evidence.rs
Original file line number Diff line number Diff line change
@@ -23,13 +23,12 @@
use std::{collections::HashMap, ffi::CStr, mem::ManuallyDrop, os::raw::c_char};

use log::{debug, error, warn};
use thiserror::Error;

use crate::{ffi_util::FFIError, rrf_call, sys, validate_str_param};

/// CdiffError enumerates all possible errors returned by this library.
#[derive(Error, Debug)]
pub enum EvidenceError {
/// Error enumerates all possible errors returned by this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid format")]
Format,

@@ -240,21 +239,21 @@ impl Evidence {
name: &str,
static_virname: *const c_char,
indicator_type: IndicatorType,
) -> Result<(), EvidenceError> {
) -> Result<(), Error> {
let meta: IndicatorMeta = IndicatorMeta { static_virname };

match indicator_type {
IndicatorType::Strong => {
self.strong
.entry(name.to_string())
.or_insert_with(Vec::new)
.or_default()
.push(meta);
}

IndicatorType::PotentiallyUnwanted => {
self.pua
.entry(name.to_string())
.or_insert_with(Vec::new)
.or_default()
.push(meta);
}

@@ -265,7 +264,7 @@ impl Evidence {
IndicatorType::Weak => {
self.weak
.entry(name.to_string())
.or_insert_with(Vec::new)
.or_default()
.push(meta);
}
}
105 changes: 105 additions & 0 deletions libclamav_rust/src/fmap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Rust interface for libclamav FMap module
*
* Copyright (C) 2023 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*
* Authors: Micah Snyder
*
* 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.
*/

use std::convert::TryFrom;

use log::{debug, error};

use crate::{sys, util::str_from_ptr};

/// Error enumerates all possible errors returned by this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid parameter: {0}")]
InvalidParameter(String),

#[error("{0} parmeter is NULL")]
NullParam(&'static str),

#[error("Offset {0} and length {1} not contained in FMap of size {2}")]
NotContained(usize, usize, usize),

#[error("FMap pointer not initialized: {0}")]
UninitializedPtr(&'static str),

#[error("Attempted to create Rust FMap interface from NULL pointer")]
Null,
}

#[derive(PartialEq, Eq, Hash, Debug)]
pub struct FMap {
fmap_ptr: *mut sys::cl_fmap_t,
}

impl TryFrom<*mut sys::cl_fmap_t> for FMap {
type Error = Error;

fn try_from(value: *mut sys::cl_fmap_t) -> Result<Self, Self::Error> {
if value.is_null() {
return Err(Error::Null);
}

Ok(FMap { fmap_ptr: value })
}
}

impl<'a> FMap {
/// Simple wrapper around C FMAP module's fmap.need() method.
pub fn need_off(&'a self, at: usize, len: usize) -> Result<&'a [u8], Error> {
// Get the need() method function pointer from the fmap.
let need_fn = match unsafe { *self.fmap_ptr }.need {
Some(ptr) => ptr,
None => return Err(Error::UninitializedPtr("need()")),
};

let ptr: *const u8 = unsafe { need_fn(self.fmap_ptr, at, len, 1) } as *const u8;

if ptr.is_null() {
let fmap_size = unsafe { *self.fmap_ptr }.len;
debug!(
"need_off at {:?} len {:?} for fmap size {:?} returned NULL",
at, len, fmap_size
);
return Err(Error::NotContained(at, len, fmap_size));
}

let slice: &[u8] = unsafe { std::slice::from_raw_parts(ptr, len) };

Ok(slice)
}

pub fn len(&self) -> usize {
unsafe { (*self.fmap_ptr).len }
}

pub fn is_empty(&self) -> bool {
unsafe { (*self.fmap_ptr).len == 0 }
}

pub fn name(&self) -> &'static str {
unsafe {
str_from_ptr((*self.fmap_ptr).name)
.unwrap_or(Some("<invalid-utf8>"))
.unwrap_or("<unnamed>")
}
}
}
46 changes: 24 additions & 22 deletions libclamav_rust/src/fuzzy_hash.rs
Original file line number Diff line number Diff line change
@@ -34,14 +34,13 @@ use image::{imageops::FilterType::Lanczos3, DynamicImage, ImageBuffer, Luma, Pix
use log::{debug, error, warn};
use num_traits::{NumCast, ToPrimitive, Zero};
use rustdct::DctPlanner;
use thiserror::Error;
use transpose::transpose;

use crate::{ffi_error, ffi_util::FFIError, rrf_call, sys, validate_str_param};

/// CdiffError enumerates all possible errors returned by this library.
#[derive(Error, Debug)]
pub enum FuzzyHashError {
/// Error enumerates all possible errors returned by this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid format")]
Format,

@@ -68,6 +67,9 @@ pub enum FuzzyHashError {

#[error("{0} parmeter is NULL")]
NullParam(&'static str),

#[error("{0} hash must be {1} characters in length")]
InvalidHashLength(&'static str, usize),
}

#[derive(PartialEq, Eq, Hash, Debug)]
@@ -81,18 +83,18 @@ pub enum FuzzyHash {
}

impl TryFrom<&str> for ImageFuzzyHash {
type Error = &'static str;
type Error = Error;

fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.len() != 16 {
return Err("Image fuzzy hash must be 16 characters in length");
return Err(Error::InvalidHashLength("ImageFuzzyHash", 16));
}

let mut hashbytes = [0; 8];
if hex::decode_to_slice(value, &mut hashbytes).is_ok() {
Ok(ImageFuzzyHash { bytes: hashbytes })
} else {
Err("Failed to decode image fuzzy hash bytes from hex to bytes")
Err(Error::FormatHashBytes(value.to_string()))
}
}
}
@@ -205,11 +207,11 @@ pub unsafe extern "C" fn _fuzzy_hash_calculate_image(
err: *mut *mut FFIError,
) -> bool {
if hash_out.is_null() {
return ffi_error!(err = err, FuzzyHashError::NullParam("hash_out"));
return ffi_error!(err = err, Error::NullParam("hash_out"));
}

let buffer = if file_bytes.is_null() {
return ffi_error!(err = err, FuzzyHashError::NullParam("file_bytes"));
return ffi_error!(err = err, Error::NullParam("file_bytes"));
} else {
slice::from_raw_parts(file_bytes, file_size)
};
@@ -223,7 +225,7 @@ pub unsafe extern "C" fn _fuzzy_hash_calculate_image(
if hash_out_len < hash_bytes.len() {
return ffi_error!(
err = err,
FuzzyHashError::InvalidParameter(format!(
Error::InvalidParameter(format!(
"hash_bytes output parameter too small to hold the hash: {} < {}",
hash_out_len,
hash_bytes.len()
@@ -257,24 +259,24 @@ impl FuzzyHashMap {
hexsig: &str,
lsig_id: u32,
subsig_id: u32,
) -> Result<(), FuzzyHashError> {
) -> Result<(), Error> {
let mut hexsig_split = hexsig.split('#');

let algorithm = match hexsig_split.next() {
Some(x) => x,
None => return Err(FuzzyHashError::Format),
None => return Err(Error::Format),
};

let hash = match hexsig_split.next() {
Some(x) => x,
None => return Err(FuzzyHashError::Format),
None => return Err(Error::Format),
};

let distance: u32 = match hexsig_split.next() {
Some(x) => match x.parse::<u32>() {
Ok(n) => n,
Err(_) => {
return Err(FuzzyHashError::FormatHammingDistance(x.to_string()));
return Err(Error::FormatHammingDistance(x.to_string()));
}
},
None => 0,
@@ -285,15 +287,15 @@ impl FuzzyHashMap {
error!(
"Non-zero hamming distances for image fuzzy hashes are not supported in this version."
);
return Err(FuzzyHashError::InvalidHammingDistance(distance));
return Err(Error::InvalidHammingDistance(distance));
}

match algorithm {
"fuzzy_img" => {
// Convert the hash string to an image fuzzy hash bytes struct
let image_fuzzy_hash = hash
.try_into()
.map_err(|e| FuzzyHashError::FormatHashBytes(format!("{}: {}", e, hash)))?;
.map_err(|e| Error::FormatHashBytes(format!("{}: {}", e, hash)))?;

let fuzzy_hash = FuzzyHash::Image(image_fuzzy_hash);

@@ -308,14 +310,14 @@ impl FuzzyHashMap {
// Then add the current meta struct to the entry.
self.hashmap
.entry(fuzzy_hash)
.or_insert_with(Vec::new)
.or_default()
.push(meta);

Ok(())
}
_ => {
error!("Unknown fuzzy hash algorithm: {}", algorithm);
Err(FuzzyHashError::UnknownAlgorithm(algorithm.to_string()))
Err(Error::UnknownAlgorithm(algorithm.to_string()))
}
}
}
@@ -412,17 +414,17 @@ impl FuzzyHashMap {
///
/// param: hash_out is an output variable
/// param: hash_out_len indicates the size of the hash_out buffer
pub fn fuzzy_hash_calculate_image(buffer: &[u8]) -> Result<Vec<u8>, FuzzyHashError> {
pub fn fuzzy_hash_calculate_image(buffer: &[u8]) -> Result<Vec<u8>, Error> {

// Load image and attempt to catch panics in case the decoders encounter unexpected issues
let result = panic::catch_unwind(|| -> Result<DynamicImage, FuzzyHashError> {
let image = image::load_from_memory(buffer).map_err(FuzzyHashError::ImageLoad)?;
let result = panic::catch_unwind(|| -> Result<DynamicImage, Error> {
let image = image::load_from_memory(buffer).map_err(Error::ImageLoad)?;
Ok(image)
});

let og_image = match result {
Ok(image) => image?,
Err(_) => return Err(FuzzyHashError::ImageLoadPanic()),
Err(_) => return Err(Error::ImageLoadPanic()),
};

// Drop the alpha channel (if exists).
6 changes: 5 additions & 1 deletion libclamav_rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -24,9 +24,13 @@
pub mod sys;

pub mod cdiff;
pub mod css_image_extract;
pub mod ctx;
pub mod evidence;
pub mod ffi_util;
pub mod fmap;
pub mod fuzzy_hash;
pub mod logging;
pub mod onenote;
pub mod scanners;
pub mod util;
pub mod css_image_extract;
285 changes: 285 additions & 0 deletions libclamav_rust/src/onenote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
/*
* Onenote document parser to extract embedded files.
*
* Copyright (C) 2023 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*
* Authors: Micah Snyder
*
* 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.
*/

use std::{
convert::TryInto,
mem, panic,
path::{Path, PathBuf},
};

use hex_literal::hex;
use log::{debug, error};
use onenote_parser;

/// Error enumerates all possible errors returned by this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid format")]
Format,

#[error("Invalid parameter: {0}")]
InvalidParameter(String),

#[error("Failed to open file: {0}, {1}")]
FailedToOpen(PathBuf, String),

#[error("Failed to get size for file: {0}")]
FailedToGetFileSize(PathBuf),

#[error("{0} parameter is NULL")]
NullParam(&'static str),

#[error("No more files to extract")]
NoMoreFiles,

#[error("Unable to parse OneNote file")]
Parse,

#[error("Failed to parse OneNote file due to a panic in the onenote_parser library")]
OneNoteParserPanic,
}

fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}

/// Struct representing a file extracted from a OneNote document.
/// This has the option of providing a file name, if one was found when extracting the file.
pub struct ExtractedFile {
pub name: Option<String>,
pub data: Vec<u8>,
}

/// Struct used for a file handle for our OneNote parser.
/// This struct is used to keep track of state for our iterator to work through the document extracting each file.
/// There are three different ways we keep track of state depending on the file format and the way in which the file was opened.
#[derive(Default)]
pub struct OneNote<'a> {
embedded_files: Vec<ExtractedFile>,
remaining_vec: Option<Vec<u8>>,
remaining: Option<&'a [u8]>,
}

// https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/8806fd18-6735-4874-b111-227b83eaac26
#[repr(packed)]
#[allow(dead_code)]
struct FileDataHeader {
guid_header: [u8; 16],
cb_length: u64,
unused: u32,
reserved: u64,
}
const SIZE_OF_FILE_DATA_HEADER: usize = mem::size_of::<FileDataHeader>();

// Hex sequence identifying the start of a file data store object.
const FILE_DATA_STORE_OBJECT: &[u8] = &hex!("e716e3bd65261145a4c48d4d0b7a9eac");

// Hex sequence identifying the start of a OneNote file.
const ONE_MAGIC: &[u8] = &hex!("e4525c7b8cd8a74daeb15378d02996d3");

impl<'a> OneNote<'a> {
/// Open a OneNote document given a slice bytes.
pub fn from_bytes(data: &'a [u8], filename: &Path) -> Result<OneNote<'a>, Error> {
debug!(
"Inspecting OneNote file for attachments from in-memory buffer of size {}-bytes named {}\n",
data.len(), filename.to_string_lossy()
);

fn parse_section_buffer(data: &[u8], filename: &Path) -> Result<Vec<ExtractedFile>, Error> {
let mut embedded_files: Vec<ExtractedFile> = vec![];
let mut parser = onenote_parser::Parser::new();

if let Ok(section) = parser.parse_section_buffer(data, filename) {
// file appears to be OneStore 2.8 `.one` file.
section.page_series().iter().for_each(|page_series| {
page_series.pages().iter().for_each(|page| {
page.contents().iter().for_each(|page_content| {
if let Some(page_outline) = page_content.outline() {
page_outline.items().iter().for_each(|outline_item| {
outline_item.element().iter().for_each(|&outline_element| {
outline_element.contents().iter().for_each(|content| {
if let Some(embedded_file) = content.embedded_file() {
let data = embedded_file.data();
let name = embedded_file.filename();

// If name is empty, set to None.
let name = if name.is_empty() {
debug!("Found unnamed attached file of size {}-bytes", data.len());
None
} else {
debug!("Found attached file '{}' of size {}-bytes", name, data.len());
Some(name.to_string())
};

embedded_files.push(ExtractedFile {
name,
data: data.to_vec(),
});
}
});
});
});
}
});
});
});
} else {
return Err(Error::Parse);
}

Ok(embedded_files)
}

// Try to parse the section buffer using the onenote_parser crate.
// Attempt to catch panics in case the parser encounter unexpected issues.
let result_result = panic::catch_unwind(|| -> Result<Vec<ExtractedFile>, Error> {
parse_section_buffer(data, filename)
});

// Check if it panicked. If no panic, grab the parse result.
let result = result_result.map_err(|_| Error::OneNoteParserPanic)?;

if let Ok(embedded_files) = result {
// Successfully parsed the OneNote file with the onenote_parser crate.
Ok(OneNote {
embedded_files,
..Default::default()
})
} else {
debug!("Unable to parse OneNote file with onenote_parser crate. Trying a different method known to work with older office 2010 OneNote files to extract attachments.");

let embedded_files: Vec<ExtractedFile> = vec![];

// Verify that the OneNote document file magic is correct.
// We don't check this for the onenote_parser crate because it does this for us, and may add support for newer OneNote file formats in the future.
let file_magic = data.get(0..16).ok_or(Error::Format)?;
if file_magic != ONE_MAGIC {
return Err(Error::Format);
}

Ok(OneNote {
embedded_files,
remaining: Some(data),
..Default::default()
})
}
}

/// Open a OneNote document given the document was provided as a slice of bytes.
pub fn next_file(&mut self) -> Option<ExtractedFile> {
debug!("Looking to extract file from OneNote section...");

let mut file_data: Option<Vec<u8>> = None;

let remaining = if let Some(remaining_in) = self.remaining {
let remaining = if let Some(pos) = find_bytes(remaining_in, FILE_DATA_STORE_OBJECT) {
let (_, remaining) = remaining_in.split_at(pos);
// Found file data store object.
remaining
} else {
return None;
};

let data_length = if let Some(x) = remaining.get(16..20) {
u32::from_le_bytes(x.try_into().unwrap()) as u64
} else {
return None;
};

let data: &[u8] = remaining
.get(SIZE_OF_FILE_DATA_HEADER..SIZE_OF_FILE_DATA_HEADER + data_length as usize)?;

file_data = Some(data.to_vec());

Some(&remaining[SIZE_OF_FILE_DATA_HEADER + (data_length as usize)..remaining.len()])
} else {
None
};

self.remaining = remaining;

file_data.map(|data| ExtractedFile { data, name: None })
}

/// Get the next file from the OneNote document using the method required for when we've read the file into a Vec.
pub fn next_file_vec(&mut self) -> Option<ExtractedFile> {
debug!("Looking to extract file from OneNote section...");

let mut file_data: Option<Vec<u8>> = None;

self.remaining_vec = if let Some(ref remaining_vec) = self.remaining_vec {
let remaining = if let Some(pos) = find_bytes(remaining_vec, FILE_DATA_STORE_OBJECT) {
let (_, remaining) = remaining_vec.split_at(pos);
// Found file data store object.
remaining
} else {
return None;
};

let data_length = if let Some(x) = remaining.get(16..20) {
u32::from_le_bytes(x.try_into().unwrap()) as u64
} else {
return None;
};

let data: &[u8] = remaining
.get(SIZE_OF_FILE_DATA_HEADER..SIZE_OF_FILE_DATA_HEADER + data_length as usize)?;

file_data = Some(data.to_vec());

Some(Vec::from(
&remaining[SIZE_OF_FILE_DATA_HEADER + (data_length as usize)..remaining.len()],
))
} else {
None
};

file_data.map(|data| ExtractedFile { data, name: None })
}

/// Get the next file from the OneNote document using the method required for the onenote_parser crate.
pub fn next_file_parser(&mut self) -> Option<ExtractedFile> {
self.embedded_files.pop()
}
}

impl<'a> Iterator for OneNote<'a> {
type Item = ExtractedFile;

fn next(&mut self) -> Option<ExtractedFile> {
// Find the next embedded file
if self.remaining.is_some() {
// Data stored in a slice.
self.next_file()
} else if self.remaining_vec.is_some() {
// Data stored in a Vec.
self.next_file_vec()
} else if !self.embedded_files.is_empty() {
// Data stored in a Vec.
self.next_file_parser()
} else {
None
}
}
}
129 changes: 129 additions & 0 deletions libclamav_rust/src/scanners.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Rust equivalent of libclamav's scanners.c module
*
* Copyright (C) 2023 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*
* Authors: Micah Snyder
*
* 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.
*/

use std::{
ffi::{c_char, CString},
path::Path,
ptr::null_mut,
};

use libc::c_void;
use log::{debug, error, warn};

use crate::{
ctx,
onenote::OneNote,
sys::{cl_error_t, cl_error_t_CL_ERROR, cl_error_t_CL_SUCCESS, cli_ctx, cli_magic_scan_buff},
};

/// Rust wrapper of libclamav's cli_magic_scan_buff() function.
/// Use magic sigs to identify the file type and then scan it.
fn magic_scan(ctx: *mut cli_ctx, buf: &[u8], name: Option<String>) -> cl_error_t {
let ptr = buf.as_ptr();
let len = buf.len();

match &name {
Some(name) => debug!("Scanning {}-byte file named {}.", len, name),
None => debug!("Scanning {}-byte unnamed file.", len),
}

// Convert name to a C string.
let name = match name {
Some(name) => name,
None => String::from(""),
};

let name_ptr: *mut c_char = match CString::new(name) {
Ok(name_cstr) => {
// into_raw() so name_cstr doesn't get dropped and
// we don't do an unsafe deref of the pointer.
name_cstr.into_raw()
}
Err(_) => null_mut(),
};

let ret = unsafe { cli_magic_scan_buff(ptr as *const c_void, len, ctx, name_ptr, 0) };

if ret != cl_error_t_CL_SUCCESS {
debug!("cli_magic_scan_buff returned error: {}", ret);
}

// Okay now safe to drop the name CString.
let _ = unsafe { CString::from_raw(name_ptr) };

ret
}

/// Scan a OneNote file for attachments
///
/// # Safety
///
/// Must be a valid ctx pointer.
#[no_mangle]
pub unsafe extern "C" fn scan_onenote(ctx: *mut cli_ctx) -> cl_error_t {
let fmap = match ctx::current_fmap(ctx) {
Ok(fmap) => fmap,
Err(e) => {
warn!("Error getting FMap from ctx: {e}");
return cl_error_t_CL_ERROR;
}
};

let file_bytes = match fmap.need_off(0, fmap.len()) {
Ok(bytes) => bytes,
Err(err) => {
error!(
"Failed to get file bytes for fmap of size {}: {err}",
fmap.len()
);
return cl_error_t_CL_ERROR;
}
};

let one = match OneNote::from_bytes(file_bytes, Path::new(fmap.name())) {
Ok(x) => x,
Err(err) => {
error!("Failed to parse OneNote file: {}", err.to_string());
return cl_error_t_CL_ERROR;
}
};

let mut scan_result = cl_error_t_CL_SUCCESS;

one.into_iter().all(|attachment| {
debug!(
"Extracted {}-byte attachment with name: {:?}",
attachment.data.len(),
attachment.name
);

let ret = magic_scan(ctx, &attachment.data, attachment.name);
if ret != cl_error_t_CL_SUCCESS {
scan_result = ret;
return false;
}

true
});

scan_result
}
70 changes: 42 additions & 28 deletions libclamav_rust/src/sys.rs
Original file line number Diff line number Diff line change
@@ -375,34 +375,36 @@ pub const cli_file_CL_TYPE_HWP3: cli_file = 550;
pub const cli_file_CL_TYPE_OOXML_HWP: cli_file = 551;
pub const cli_file_CL_TYPE_PS: cli_file = 552;
pub const cli_file_CL_TYPE_EGG: cli_file = 553;
pub const cli_file_CL_TYPE_PART_ANY: cli_file = 554;
pub const cli_file_CL_TYPE_PART_HFSPLUS: cli_file = 555;
pub const cli_file_CL_TYPE_MBR: cli_file = 556;
pub const cli_file_CL_TYPE_HTML: cli_file = 557;
pub const cli_file_CL_TYPE_MAIL: cli_file = 558;
pub const cli_file_CL_TYPE_SFX: cli_file = 559;
pub const cli_file_CL_TYPE_ZIPSFX: cli_file = 560;
pub const cli_file_CL_TYPE_RARSFX: cli_file = 561;
pub const cli_file_CL_TYPE_7ZSFX: cli_file = 562;
pub const cli_file_CL_TYPE_CABSFX: cli_file = 563;
pub const cli_file_CL_TYPE_ARJSFX: cli_file = 564;
pub const cli_file_CL_TYPE_EGGSFX: cli_file = 565;
pub const cli_file_CL_TYPE_NULSFT: cli_file = 566;
pub const cli_file_CL_TYPE_AUTOIT: cli_file = 567;
pub const cli_file_CL_TYPE_ISHIELD_MSI: cli_file = 568;
pub const cli_file_CL_TYPE_ISO9660: cli_file = 569;
pub const cli_file_CL_TYPE_DMG: cli_file = 570;
pub const cli_file_CL_TYPE_GPT: cli_file = 571;
pub const cli_file_CL_TYPE_APM: cli_file = 572;
pub const cli_file_CL_TYPE_XDP: cli_file = 573;
pub const cli_file_CL_TYPE_XML_WORD: cli_file = 574;
pub const cli_file_CL_TYPE_XML_XL: cli_file = 575;
pub const cli_file_CL_TYPE_XML_HWP: cli_file = 576;
pub const cli_file_CL_TYPE_HWPOLE2: cli_file = 577;
pub const cli_file_CL_TYPE_MHTML: cli_file = 578;
pub const cli_file_CL_TYPE_LNK: cli_file = 579;
pub const cli_file_CL_TYPE_OTHER: cli_file = 580;
pub const cli_file_CL_TYPE_IGNORED: cli_file = 581;
pub const cli_file_CL_TYPE_ONENOTE: cli_file = 554;
pub const cli_file_CL_TYPE_PART_ANY: cli_file = 555;
pub const cli_file_CL_TYPE_PART_HFSPLUS: cli_file = 556;
pub const cli_file_CL_TYPE_MBR: cli_file = 557;
pub const cli_file_CL_TYPE_HTML: cli_file = 558;
pub const cli_file_CL_TYPE_MAIL: cli_file = 559;
pub const cli_file_CL_TYPE_SFX: cli_file = 560;
pub const cli_file_CL_TYPE_ZIPSFX: cli_file = 561;
pub const cli_file_CL_TYPE_RARSFX: cli_file = 562;
pub const cli_file_CL_TYPE_7ZSFX: cli_file = 563;
pub const cli_file_CL_TYPE_CABSFX: cli_file = 564;
pub const cli_file_CL_TYPE_ARJSFX: cli_file = 565;
pub const cli_file_CL_TYPE_EGGSFX: cli_file = 566;
pub const cli_file_CL_TYPE_NULSFT: cli_file = 567;
pub const cli_file_CL_TYPE_AUTOIT: cli_file = 568;
pub const cli_file_CL_TYPE_ISHIELD_MSI: cli_file = 569;
pub const cli_file_CL_TYPE_ISO9660: cli_file = 570;
pub const cli_file_CL_TYPE_DMG: cli_file = 571;
pub const cli_file_CL_TYPE_GPT: cli_file = 572;
pub const cli_file_CL_TYPE_APM: cli_file = 573;
pub const cli_file_CL_TYPE_XDP: cli_file = 574;
pub const cli_file_CL_TYPE_XML_WORD: cli_file = 575;
pub const cli_file_CL_TYPE_XML_XL: cli_file = 576;
pub const cli_file_CL_TYPE_XML_HWP: cli_file = 577;
pub const cli_file_CL_TYPE_HWPOLE2: cli_file = 578;
pub const cli_file_CL_TYPE_MHTML: cli_file = 579;
pub const cli_file_CL_TYPE_LNK: cli_file = 580;
pub const cli_file_CL_TYPE_UDF: cli_file = 581;
pub const cli_file_CL_TYPE_OTHER: cli_file = 582;
pub const cli_file_CL_TYPE_IGNORED: cli_file = 583;
pub type cli_file = ::std::os::raw::c_uint;
pub use self::cli_file as cli_file_t;
#[repr(C)]
@@ -612,6 +614,7 @@ pub struct recursion_level_tag {
}
pub type recursion_level_t = recursion_level_tag;
pub type evidence_t = *mut ::std::os::raw::c_void;
pub type onedump_t = *mut ::std::os::raw::c_void;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct cli_ctx_tag {
@@ -706,6 +709,7 @@ pub struct cl_engine {
pub tmpdir: *mut ::std::os::raw::c_char,
pub keeptmp: u32,
pub engine_options: u64,
pub cache_size: u32,
pub maxscantime: u32,
pub maxscansize: u64,
pub maxfilesize: u64,
@@ -1170,6 +1174,16 @@ extern "C" {
}
pub type css_image_extractor_t = *mut ::std::os::raw::c_void;
pub type css_image_handle_t = *mut ::std::os::raw::c_void;
extern "C" {
#[doc = " @brief Convenience wrapper for cli_magic_scan_nested_fmap_type().\n\n Creates an fmap and calls cli_magic_scan_nested_fmap_type() for you, with type CL_TYPE_ANY.\n\n @param buffer Pointer to the buffer to be scanned.\n @param length Size in bytes of the buffer being scanned.\n @param ctx Scanning context structure.\n @param name (optional) Original name of the file (to set fmap name metadata)\n @param attributes Layer attributes of the file being scanned (is it normalized, decrypted, etc)\n @return int CL_SUCCESS, or an error code."]
pub fn cli_magic_scan_buff(
buffer: *const ::std::os::raw::c_void,
length: usize,
ctx: *mut cli_ctx,
name: *const ::std::os::raw::c_char,
attributes: u32,
) -> cl_error_t;
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct re_guts {
18 changes: 17 additions & 1 deletion libclamav_rust/src/util.rs
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@
* MA 02110-1301, USA.
*/

use std::fs::File;
use std::{ffi::CStr, fs::File};

/// Obtain a std::fs::File from an i32 in a platform-independent manner.
///
@@ -46,3 +46,19 @@ pub fn file_from_fd_or_handle(fd: i32) -> File {
#[cfg(not(any(windows, unix)))]
compile_error!("implemented only for unix and windows targets")
}

/// Get a string from a pointer
///
/// # Safety
///
/// The caller is responsible for making sure the lifetime of the pointer
/// exceeds the lifetime of the output string.
///
/// ptr must be a valid pointer to a C string.
pub unsafe fn str_from_ptr(ptr: *const i8) -> Result<Option<&'static str>, std::str::Utf8Error> {
if ptr.is_null() {
return Ok(None);
}

Some(unsafe { CStr::from_ptr(ptr) }.to_str()).transpose()
}
2 changes: 1 addition & 1 deletion unit_tests/clamscan/fuzzy_img_hash_test.py
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ def test_sigs_bad_hash(self):

expected_stderr = [
'LibClamAV Error: Failed to load',
'Invalid hash: Image fuzzy hash must be 16 characters in length: abcdef',
'Invalid hash: ImageFuzzyHash hash must be 16 characters in length: abcdef',
]
unexpected_stdout = [
'logo.png.bad.UNOFFICIAL FOUND',

0 comments on commit 27a9c40

Please sign in to comment.