Lines of
src/utils.rs
from check-in 512369f93e
that are changed by the sequence of edits moving toward
check-in 0acb6536ae:
1: //! Utility functions and types for the application.
2:
3: use crate::Cursor;
4:
5: use std::{
6: borrow::Cow,
7: sync::LazyLock,
8: };
9:
10: use html_escape::encode_text;
11: use regex::{
12: Regex,
13: RegexBuilder,
14: };
15: use stacked_errors::{
16: bail,
17: Result,
18: };
19:
20: pub static RE_DOMAIN: LazyLock<Regex> = LazyLock::new(|| {
21: Regex::new(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$").expect("Invalid domain regex")
22: });
23: pub static RE_CLOSING: LazyLock<Regex> = LazyLock::new(|| {
24: RegexBuilder::new(r"</[ \t]*(pre|code)[ \t]*>")
25: .case_insensitive(true).build().expect("Invalid closing tag regex")
26: });
27:
28: /// Stores binary attachment data and metadata for Telegram messages.
29: /// The data is wrapped in a `Cursor<Vec<u8>>` for efficient streaming,
30: /// while `name` holds the filename or display name of the attachment.
31: #[derive(Debug)]
32: pub struct Attachment {
33: pub data: Cursor<Vec<u8>>,
34: pub name: String,
35: }
36:
37: /// Validates text to ensure it doesn't break Telegram's preformatted blocks.
38: ///
39: /// Escapes HTML special characters to prevent injection.
40: ///
41: /// # Arguments
42: /// * `text` - Text to validate and escape.
43: ///
44: /// # Returns
45: /// * `Result<Cow<'a, str>>` - Escaped text or error if invalid.
46: ///
47: /// # Errors
48: /// Returns an error if the text contains Telegram closing tags (`</pre>`, `</code>`).
49: pub fn validate <'a>(text: &'a str) -> Result<Cow<'a, str>> {
50: if RE_CLOSING.is_match(text) {
512369f93e 2026-08-01 51: bail!("Telegram closing tag found.");
52: } else {
53: Ok(encode_text(text))
512369f93e 2026-08-01 54: }
512369f93e 2026-08-01 55: }