From 06d0b635a9822458c2ec65b367e7a4717ce391cd Mon Sep 17 00:00:00 2001 From: draccut Date: Tue, 15 Sep 2026 14:59:17 +0000 Subject: [PATCH] Dateien nach "/" hochladen --- Cargo.toml | 8 ++ detect_filetype.rs | 37 +++++++ language.rs | 19 ++++ main.rs | 256 +++++++++++++++++++++++++++++++++++++++++++++ state.rs | 27 +++++ 5 files changed, 347 insertions(+) create mode 100644 Cargo.toml create mode 100644 detect_filetype.rs create mode 100644 language.rs create mode 100644 main.rs create mode 100644 state.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ddc0d39 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "CCAT" +version = "2.8.6" +edition = "2024" +description = "Simple syntax-highlighting cat clone written in Rust" + +[dependencies] + diff --git a/detect_filetype.rs b/detect_filetype.rs new file mode 100644 index 0000000..5d3c259 --- /dev/null +++ b/detect_filetype.rs @@ -0,0 +1,37 @@ +use crate::language::Language; +use std::path::Path; + +const DOCKERFILE_KEY_WORD: &str = "Dockerfile"; +const DOCKER_COMPOSE_KEYWORD: &str = "docker-compose.yml"; + +pub fn detect_language(filename: &str) -> Language { + if filename.is_empty() { + return Language::Plain; + } + let path = Path::new(filename); + if let Some(ext) = path.extension().and_then(|s| s.to_str()) { + match ext { + "py" => return Language::Python, + "js" => return Language::JavaScript, + "c" | "h" | "cpp" | "ino" => return Language::Clang, + "md" => return Language::Markdown, + "html" | "htm" | "xml" => return Language::Html, + "sh" => return Language::Shell, + "asm" => return Language::AsmX86, + "a51" => return Language::Asm8051, + "sql" => return Language::Sql, + "rs" => return Language::Rust, + "log" => return Language::LogFile, + "java" => return Language::LangJava, + _ => {} + } + } + let base = path.file_name().and_then(|s| s.to_str()).unwrap_or(filename); + if base.eq_ignore_ascii_case(DOCKERFILE_KEY_WORD) { + return Language::Docker; + } + if base.eq_ignore_ascii_case(DOCKER_COMPOSE_KEYWORD) { + return Language::LangDockerCompose; + } + Language::Plain +} diff --git a/language.rs b/language.rs new file mode 100644 index 0000000..c55956b --- /dev/null +++ b/language.rs @@ -0,0 +1,19 @@ +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Language { + Plain, + Python, + JavaScript, + Clang, + Markdown, + Html, + Docker, + Shell, + AsmX86, + Asm8051, + Sql, + Rust, + LangDockerCompose, + LangJava, + LogFile +} + diff --git a/main.rs b/main.rs new file mode 100644 index 0000000..7029a93 --- /dev/null +++ b/main.rs @@ -0,0 +1,256 @@ +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 [Optionen] +@VERSION 2.9.4 Rust + +c + +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 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 = 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"", ""); + } +} diff --git a/state.rs b/state.rs new file mode 100644 index 0000000..5263376 --- /dev/null +++ b/state.rs @@ -0,0 +1,27 @@ +#[derive(Debug)] +pub struct State { + pub line_nr: u64, + pub flag_nr: bool, + pub print_binary: bool, + pub remove_all_ansi: bool, + pub html_color: bool, + pub hex: bool, + pub stop_at_line: u64, + pub to_c_array: bool, +} + +impl State { + pub fn new() -> Self { + Self { + line_nr: 0, + flag_nr: false, + print_binary: false, + remove_all_ansi: false, + html_color: false, + hex: false, + stop_at_line: 0xFFFFFFFFFFFFF000, + to_c_array: false, + } + } +} +