use anyhow::Result; use gethostname::gethostname; use local_ip_address::local_ip; use dns_lookup::lookup_addr; use std::io; use std::io::Write; use std::env; use std::process; use uzers::{get_current_uid, get_current_gid, get_current_username, get_current_groupname}; use crate::meta_plugin::MetaPlugin; #[derive(Debug, Clone, Default)] pub struct CwdMetaPlugin { meta_name: String, } #[derive(Debug, Clone, Default)] pub struct BinaryMetaPlugin { meta_name: String, buffer: Vec, max_buffer_size: usize, } impl BinaryMetaPlugin { pub fn new() -> BinaryMetaPlugin { BinaryMetaPlugin { meta_name: "binary".to_string(), buffer: Vec::new(), max_buffer_size: 4096, // 4KB } } /// Detect if data is binary or text /// Returns true if data is likely binary, false if likely text fn is_binary(data: &[u8]) -> bool { if data.is_empty() { return false; } // First check for known binary file signatures if Self::has_binary_signature(data) { return true; } // Check for UTF-16 BOM (text) if data.len() >= 2 { if (data[0] == 0xFF && data[1] == 0xFE) || (data[0] == 0xFE && data[1] == 0xFF) { return false; // UTF-16 with BOM is text } } // Check for UTF-8 BOM (text) if data.len() >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF { return false; // UTF-8 with BOM is text } // Check if it's valid UTF-8 if std::str::from_utf8(data).is_ok() { // Valid UTF-8, check printable character ratio return Self::calculate_printable_ratio(data) < 0.7; } // Not valid UTF-8, check if it might be UTF-16 without BOM if Self::looks_like_utf16(data) { return false; // Likely UTF-16 text } // Check for TAR format (special case with no magic number) if Self::looks_like_tar(data) { return true; } // Final fallback: check printable character ratio Self::calculate_printable_ratio(data) < 0.7 } /// Check for known binary file signatures fn has_binary_signature(data: &[u8]) -> bool { // Define binary file signatures with their minimum required lengths let signatures: &[(&[u8], usize)] = &[ // Image formats (&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], 8), // PNG (&[0xFF, 0xD8, 0xFF], 3), // JPEG (various subtypes) (&[0x47, 0x49, 0x46, 0x38, 0x37, 0x61], 6), // GIF87a (&[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 6), // GIF89a (&[0x42, 0x4D], 2), // BMP (&[0x00, 0x00, 0x01, 0x00], 4), // ICO (&[0x49, 0x49, 0x2A, 0x00], 4), // TIFF (little endian) (&[0x4D, 0x4D, 0x00, 0x2A], 4), // TIFF (big endian) (&[0x52, 0x49, 0x46, 0x46], 4), // WebP (RIFF container) (&[0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20], 8), // JPEG 2000 // Audio/Video formats (&[0x49, 0x44, 0x33], 3), // MP3 with ID3v2 (&[0xFF, 0xFB], 2), // MP3 (&[0xFF, 0xF3], 2), // MP3 (&[0xFF, 0xF2], 2), // MP3 (&[0x4F, 0x67, 0x67, 0x53], 4), // OGG (&[0x66, 0x74, 0x79, 0x70], 4), // MP4/M4A/MOV (at offset 4) (&[0x52, 0x49, 0x46, 0x46], 4), // WAV/AVI (RIFF) (&[0x46, 0x4C, 0x56], 3), // FLV (&[0x1A, 0x45, 0xDF, 0xA3], 4), // MKV/WebM // Archive formats (&[0x50, 0x4B, 0x03, 0x04], 4), // ZIP (&[0x50, 0x4B, 0x05, 0x06], 4), // ZIP (empty) (&[0x50, 0x4B, 0x07, 0x08], 4), // ZIP (spanned) (&[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00], 7), // RAR v1.5+ (&[0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00], 8), // RAR v5.0+ (&[0x1F, 0x8B], 2), // GZIP (&[0x42, 0x5A, 0x68], 3), // BZIP2 (&[0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00], 6), // XZ (&[0x28, 0xB5, 0x2F, 0xFD], 4), // Zstandard (&[0x04, 0x22, 0x4D, 0x18], 4), // LZ4 (&[0x1F, 0x9D], 2), // LZW compressed (&[0x1F, 0xA0], 2), // LZH compressed (&[0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 6), // 7-Zip // Document formats (&[0x25, 0x50, 0x44, 0x46], 4), // PDF (&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1], 8), // MS Office (OLE) (&[0x50, 0x4B, 0x03, 0x04], 4), // Office Open XML (also ZIP) (&[0x7B, 0x5C, 0x72, 0x74, 0x66], 5), // RTF // Executables and object files (&[0x7F, 0x45, 0x4C, 0x46], 4), // ELF (&[0x4D, 0x5A], 2), // Windows PE/DOS (&[0xCA, 0xFE, 0xBA, 0xBE], 4), // Mach-O (big endian) (&[0xFE, 0xED, 0xFA, 0xCE], 4), // Mach-O 32-bit (little endian) (&[0xFE, 0xED, 0xFA, 0xCF], 4), // Mach-O 64-bit (little endian) (&[0xCE, 0xFA, 0xED, 0xFE], 4), // Mach-O 32-bit (big endian) (&[0xCF, 0xFA, 0xED, 0xFE], 4), // Mach-O 64-bit (big endian) (&[0xCA, 0xFE, 0xBA, 0xBE], 4), // Java class file (&[0xDE, 0xC0, 0x17, 0x0B], 4), // Dalvik executable // Database formats (&[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6F, 0x72, 0x6D, 0x61, 0x74, 0x20, 0x33, 0x00], 16), // SQLite (&[0x00, 0x01, 0x00, 0x00], 4), // Palm Database // Font formats (&[0x00, 0x01, 0x00, 0x00, 0x00], 5), // TrueType (&[0x4F, 0x54, 0x54, 0x4F], 4), // OpenType (&[0x77, 0x4F, 0x46, 0x46], 4), // WOFF (&[0x77, 0x4F, 0x46, 0x32], 4), // WOFF2 // Virtual machine formats (&[0x76, 0x6D, 0x64, 0x6B], 4), // VMDK (&[0x3C, 0x3C, 0x3C, 0x20, 0x4F, 0x72, 0x61, 0x63, 0x6C, 0x65, 0x20, 0x56, 0x4D, 0x20, 0x56, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6C, 0x42, 0x6F, 0x78, 0x20, 0x44, 0x69, 0x73, 0x6B, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x20, 0x3E, 0x3E, 0x3E], 39), // VirtualBox VDI // Disk image formats (&[0xEB, 0x3C, 0x90], 3), // FAT12/16/32 (&[0xEB, 0x58, 0x90], 3), // FAT32 (&[0x55, 0xAA], 2), // Boot sector (at offset 510) // Other binary formats (&[0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A], 8), // AR archive (&[0x78, 0x01], 2), // zlib (default compression) (&[0x78, 0x9C], 2), // zlib (best compression) (&[0x78, 0xDA], 2), // zlib (fast compression) (&[0x62, 0x76, 0x78, 0x32], 4), // LZFSE ]; for (signature, min_len) in signatures { if data.len() >= *min_len && data.starts_with(signature) { return true; } } // Special case: check for ftyp box in MP4/MOV files (at offset 4) if data.len() >= 8 && &data[4..8] == b"ftyp" { return true; } false } /// Check if data looks like UTF-16 without BOM fn looks_like_utf16(data: &[u8]) -> bool { if data.len() < 4 || data.len() % 2 != 0 { return false; } let mut zero_count = 0; let pairs = data.len() / 2; // Check if every other byte is zero (indicating UTF-16) for i in 0..pairs { if data[i * 2 + 1] == 0 { zero_count += 1; } } // If more than 50% of odd positions are zero, might be UTF-16 zero_count as f64 / pairs as f64 > 0.5 } /// Check if data looks like a TAR archive fn looks_like_tar(data: &[u8]) -> bool { if data.len() < 512 { return false; } // TAR header structure validation // Filename should not start with null if data[0] == 0 { return false; } // Check file mode field (should be octal digits) for i in 100..108 { if data[i] != 0 && (data[i] < b'0' || data[i] > b'7') && data[i] != b' ' { return false; } } // Check checksum field (should be octal digits or spaces) for i in 148..156 { if data[i] != 0 && (data[i] < b'0' || data[i] > b'7') && data[i] != b' ' { return false; } } // Check magic field for POSIX TAR if data.len() >= 265 { let magic = &data[257..262]; if magic == b"ustar" { return true; } } // Additional heuristic: check if the structure looks reasonable let has_reasonable_structure = data[0] != 0 && // Filename starts data[100..108].iter().all(|&b| b == 0 || (b >= b'0' && b <= b'7') || b == b' '); // Mode field has_reasonable_structure } /// Calculate the ratio of printable characters in the data fn calculate_printable_ratio(data: &[u8]) -> f64 { let printable_count = data.iter().filter(|&&b| { b.is_ascii_graphic() || b.is_ascii_whitespace() }).count(); printable_count as f64 / data.len() as f64 } } impl MetaPlugin for BinaryMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { let is_binary = Self::is_binary(&self.buffer); Ok(if is_binary { "true".to_string() } else { "false".to_string() }) } fn update(&mut self, data: &[u8]) { // Only collect up to max_buffer_size let remaining_capacity = self.max_buffer_size.saturating_sub(self.buffer.len()); if remaining_capacity > 0 { let bytes_to_copy = std::cmp::min(data.len(), remaining_capacity); self.buffer.extend_from_slice(&data[..bytes_to_copy]); } } fn meta_name(&mut self) -> String { self.meta_name.clone() } } impl CwdMetaPlugin { pub fn new() -> CwdMetaPlugin { CwdMetaPlugin { meta_name: "cwd".to_string(), } } } impl MetaPlugin for CwdMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { match env::current_dir() { Ok(path) => Ok(path.to_string_lossy().to_string()), Err(_) => Ok("unknown".to_string()), } } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct UidMetaPlugin { meta_name: String, } impl UidMetaPlugin { pub fn new() -> UidMetaPlugin { UidMetaPlugin { meta_name: "uid".to_string(), } } } impl MetaPlugin for UidMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { Ok(get_current_uid().to_string()) } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct UserMetaPlugin { meta_name: String, } impl UserMetaPlugin { pub fn new() -> UserMetaPlugin { UserMetaPlugin { meta_name: "user".to_string(), } } } impl MetaPlugin for UserMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { match get_current_username() { Some(username) => Ok(username.to_string_lossy().to_string()), None => Ok("unknown".to_string()), } } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct GidMetaPlugin { meta_name: String, } impl GidMetaPlugin { pub fn new() -> GidMetaPlugin { GidMetaPlugin { meta_name: "gid".to_string(), } } } impl MetaPlugin for GidMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { Ok(get_current_gid().to_string()) } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct GroupMetaPlugin { meta_name: String, } impl GroupMetaPlugin { pub fn new() -> GroupMetaPlugin { GroupMetaPlugin { meta_name: "group".to_string(), } } } impl MetaPlugin for GroupMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { match get_current_groupname() { Some(groupname) => Ok(groupname.to_string_lossy().to_string()), None => Ok("unknown".to_string()), } } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct ShellMetaPlugin { meta_name: String, } impl ShellMetaPlugin { pub fn new() -> ShellMetaPlugin { ShellMetaPlugin { meta_name: "shell".to_string(), } } } impl MetaPlugin for ShellMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { match env::var("SHELL") { Ok(shell) => Ok(shell), Err(_) => Ok("unknown".to_string()), } } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct ShellPidMetaPlugin { meta_name: String, } impl ShellPidMetaPlugin { pub fn new() -> ShellPidMetaPlugin { ShellPidMetaPlugin { meta_name: "shell_pid".to_string(), } } } impl MetaPlugin for ShellPidMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { match env::var("PPID") { Ok(ppid) => Ok(ppid), Err(_) => Ok(process::id().to_string()), } } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct KeepPidMetaPlugin { meta_name: String, } impl KeepPidMetaPlugin { pub fn new() -> KeepPidMetaPlugin { KeepPidMetaPlugin { meta_name: "keep_pid".to_string(), } } } impl MetaPlugin for KeepPidMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { Ok(process::id().to_string()) } fn update(&mut self, _data: &[u8]) { // No update needed } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct HostnameMetaPlugin { meta_name: String, } impl HostnameMetaPlugin { pub fn new() -> HostnameMetaPlugin { HostnameMetaPlugin { meta_name: "hostname".to_string(), } } } impl MetaPlugin for HostnameMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { match gethostname().into_string() { Ok(hostname) => Ok(hostname), Err(_) => Ok("unknown".to_string()), } } fn update(&mut self, _data: &[u8]) { // No update needed for hostname } fn meta_name(&mut self) -> String { self.meta_name.clone() } } #[derive(Debug, Clone, Default)] pub struct FullHostnameMetaPlugin { meta_name: String, } impl FullHostnameMetaPlugin { pub fn new() -> FullHostnameMetaPlugin { FullHostnameMetaPlugin { meta_name: "full_hostname".to_string(), } } } impl MetaPlugin for FullHostnameMetaPlugin { fn is_internal(&self) -> bool { true } fn create(&self) -> Result> { Ok(Box::new(io::sink())) } fn finalize(&mut self) -> io::Result { // Try to get the FQDN through reverse DNS lookup match local_ip() { Ok(my_local_ip) => { match lookup_addr(&my_local_ip) { Ok(hostname) => Ok(hostname), Err(_) => { // Fall back to regular hostname if reverse DNS fails match gethostname().into_string() { Ok(hostname) => Ok(hostname), Err(_) => Ok("unknown".to_string()), } } } } Err(_) => { // Fall back to regular hostname if we can't get local IP match gethostname().into_string() { Ok(hostname) => Ok(hostname), Err(_) => Ok("unknown".to_string()), } } } } fn update(&mut self, _data: &[u8]) { // No update needed for full hostname } fn meta_name(&mut self) -> String { self.meta_name.clone() } }