Files
XCCAT/main.rs
T
2026-09-15 14:59:17 +00:00

257 lines
7.1 KiB
Rust

use std::env;
use std::fs;
mod language;
mod state;
mod detect_filetype;
mod highlighter;
mod keywords;
mod specials;
mod col;
mod ccat_markdown;
use crate::state::State;
use crate::detect_filetype::detect_language;
use crate::highlighter::{highlight_line, print_binary_file};
use crate::col::HighlightConfig;
use crate::language::Language;
use crate::ccat_markdown::{md_html_main, md_ansi_main};
use crate::specials::{print_hex_file, log_highlight_line, to_c_array};
const INFO_TEXT: &str = r#"
Syntax: ccat <datei> [Optionen]
@VERSION 2.9.4 Rust
c <filename> <options ...>
Optionen:
n = Zeilennummern anzeigen
b = Binärdatei als Hex-Dump ausgeben
r = Remove all ANSI (save mode)
h = print as HTML Source
a = print file as C Hexadezimal Array
x = print as hex dump
-s, --stop <nr> letzte auszugebende Zeile
"#;
fn read_file_with_hex_fallback(path: &str, output: &mut String) {
output.clear();
let bytes = match fs::read(path) {
Ok(data) => data,
Err(e) => {
eprintln!("Fehler beim Lesen von '{}': {}", path, e);
std::process::exit(1);
}
};
output.reserve(bytes.len() * 2);
let mut i = 0;
while i < bytes.len() {
if let Some((ch, len)) = decode_next_utf8_char(&bytes[i..]) {
output.push(ch);
i += len;
} else {
output.push_str(&format!(" 0x{:02X}", bytes[i]));
i += 1;
}
}
}
fn decode_next_utf8_char(slice: &[u8]) -> Option<(char, usize)> {
if slice.is_empty() {
return None;
}
let byte = slice[0];
if matches!(byte, b'\n' | b'\r' | b'\t') {
return Some((byte as char, 1));
}
if (32..=126).contains(&byte) {
return Some((byte as char, 1));
}
if byte >= 128 {
for len in 2..=4 {
if len > slice.len() {
break;
}
if let Ok(s) = std::str::from_utf8(&slice[..len]) {
if let Some(ch) = s.chars().next() {
if ch.len_utf8() == len {
if !ch.is_control() {
return Some((ch, len));
}
}
}
}
}
}
None
}
fn remove_ansi_escapes(input: &str, output: &mut String) {
output.clear();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == 0x1B {
if i + 1 < bytes.len() && bytes[i + 1] == b'[' {
i += 2;
while i < bytes.len() {
let b = bytes[i];
i += 1;
if (b'A'..=b'Z').contains(&b) || (b'a'..=b'z').contains(&b) || (b'@'..=b'~').contains(&b) {
break;
}
}
continue;
}
}
output.push(bytes[i] as char);
i += 1;
}
}
fn get_int_by_string(s: String) -> u64
{
let num: u64 = s.parse().unwrap();
return num;
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() == 1 {
print!("{}", INFO_TEXT);
return;
}
let mut state = State::new();
if args.len() >= 3 {
for ch in args[2].chars() {
match ch {
'n' => state.flag_nr = true,
'b' => state.print_binary = true,
'r' => state.remove_all_ansi = true,
'h' => state.html_color = true,
'x' => state.hex = true,
'a' => state.to_c_array = true,
_ => {}
}
}
if args[2] == "-s" || args[2] == "--stop" {
state.stop_at_line = get_int_by_string(args[3].clone());
} else {
state.stop_at_line = 0xFFFFFFFFFFFFF000;
}
}
let filename = &args[1];
let lang = detect_language(filename);
let mut source = String::new();
let mut source_len = 0x1000;
if !state.print_binary {
read_file_with_hex_fallback(filename, &mut source);
source_len = source.len();
}
let mut output = String::with_capacity(source_len * 2);
let mut jump: bool = false;
let config = if state.html_color {
HighlightConfig::html_colors()
} else {
HighlightConfig::default()
};
if state.hex {
print_hex_file(filename, &mut state, &config);
jump = true;
}
if lang == Language::LogFile {
for line in source.lines() {
log_highlight_line(line, &mut state, &config);
}
jump = true;
}
if state.to_c_array {
to_c_array(filename, &config);
jump = true;
}
if state.print_binary {
print_binary_file(filename, &mut output, &mut state, &config);
jump = true;
}
if state.remove_all_ansi {
remove_ansi_escapes(&source, &mut output);
jump = true;
}
if !jump {
if lang == Language::Markdown {
if state.html_color {
md_html_main(&source, &mut output);
} else {
md_ansi_main(&source, &mut output);
}
} else {
for line in source.lines() {
let stop_flag: bool = highlight_line(line, lang, &config, &mut output, &mut state);
if stop_flag {
break;
}
}
}
}
println!("{}", output);
}
#[cfg(test)]
mod tests {
use super::*;
fn test_with_file(filename: &str, content: &[u8], expected: &str) {
let filepath = format!("/tmp/{}", filename);
fs::write(&filepath, content).unwrap_or_else(|e| {
panic!("Konnte Testdatei nicht erstellen {}: {}", filepath, e);
});
let mut result = String::new();
read_file_with_hex_fallback(&filepath, &mut result);
assert_eq!(result, expected, "Testdatei {} fehlgeschlagen", filename);
let _ = fs::remove_file(&filepath);
}
#[test]
fn test_unicode_and_hex() {
test_with_file(
"rust_test_unicode.bin",
b"Hello \xC3\xA4\xC3\xB6\xC3\xBC\xC3\x9F \xE2\x82\xAC \xF0\x9F\x9A\x80 World!\n\x00\x07\xFF",
"Hello äöüß € 🚀 World!\n 0x00 0x07 0xFF",
);
}
#[test]
fn test_invalid_utf8_sequences() {
test_with_file(
"rust_test_invalid.bin",
b"Valid text \xFF\xFE\xFD invalid again \xC3( broken \xE2\x82\xFF end",
"Valid text 0xFF 0xFE 0xFD invalid again 0xC3( broken 0xE2 0x82 0xFF end",
);
}
#[test]
fn test_only_printable_text() {
test_with_file(
"rust_test_text.txt",
b"Hello World!\nThis is a test.\nLine 3 with umlauts: \xC3\xA4\xC3\xB6\xC3\xBC\xC3\x9F",
"Hello World!\nThis is a test.\nLine 3 with umlauts: äöüß",
);
}
#[test]
fn test_control_characters() {
test_with_file(
"rust_test_control.bin",
b"Line1\nLine2\r\nTab\t\x00\x01\x02\x1F\x7F\xFF",
"Line1\nLine2\r\nTab\t 0x00 0x01 0x02 0x1F 0x7F 0xFF",
);
}
#[test]
fn test_empty_file() {
test_with_file("rust_test_empty.bin", b"", "");
}
}