257 lines
7.1 KiB
Rust
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"", "");
|
|
}
|
|
}
|