#[derive(Default)]
pub struct StringInjector {
entries: Vec<Entry>,
}
#[derive(Debug)]
enum Entry {
Insert(usize, String),
Replace(usize, usize, String),
}
impl Entry {
fn sort_key(&self) -> (usize, u8) {
match self {
Entry::Insert(offset, _) => (*offset, 0),
Entry::Replace(start, _, _) => (*start, 1),
}
}
}
impl StringInjector {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn insert(&mut self, byte_offset: usize, text: impl Into<String>) {
self.entries.push(Entry::Insert(byte_offset, text.into()));
}
pub fn replace(&mut self, start: usize, end: usize, text: impl Into<String>) {
debug_assert!(end >= start, "replace: end ({end}) < start ({start})");
self.entries.push(Entry::Replace(start, end, text.into()));
}
pub fn apply(mut self, source: &str) -> String {
self.entries.sort_by_key(|a| a.sort_key());
let mut result = String::with_capacity(source.len() * 2);
let mut cursor = 0usize;
for entry in &self.entries {
let (start, end, text) = match entry {
Entry::Insert(offset, text) => (*offset, *offset, text.as_str()),
Entry::Replace(start, end, text) => (*start, *end, text.as_str()),
};
debug_assert!(
start >= cursor && end <= source.len(),
"StringInjector: range [{start}..{end}) out of bounds \
(cursor={cursor}, len={})",
source.len(),
);
result.push_str(&source[cursor..start]);
result.push_str(text);
cursor = end;
}
result.push_str(&source[cursor..]);
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_injector_single_injection() {
let source = "fn main() {\n println!(\"hello\");\n}\n";
let mut inj = StringInjector::new();
inj.insert(11, " let _guard = enter();");
let result = inj.apply(source);
assert!(result.contains("let _guard = enter();"));
}
#[test]
fn string_injector_multiple_injections() {
let source = "fn a() {\n 1\n}\nfn b() {\n 2\n}\n";
let mut inj = StringInjector::new();
inj.insert(8, " guard_a;");
inj.insert(25, " guard_b;");
let result = inj.apply(source);
assert!(result.contains("guard_a"));
assert!(result.contains("guard_b"));
}
#[test]
fn string_injector_preserves_formatting() {
let source = "fn weird( ) {\n x + y\n}\n";
let mut inj = StringInjector::new();
inj.insert(14, " let _g = 1;");
let result = inj.apply(source);
assert!(result.starts_with("fn weird( ) {"));
assert!(result.contains(" x + y\n"));
}
#[test]
fn replace_removes_original_text() {
let source = "aaa\nbbb\nccc\n";
let mut inj = StringInjector::new();
inj.replace(4, 8, "XXX\n");
let result = inj.apply(source);
assert_eq!(result, "aaa\nXXX\nccc\n");
}
#[test]
fn replace_allocator_wrapping() {
let source = "#[global_allocator]\nstatic ALLOC: MyAlloc = MyAlloc;\n\nfn main() {}\n";
let mut inj = StringInjector::new();
let item_end = source.find(";\n").unwrap() + 2;
inj.replace(0, item_end, "#[global_allocator]\nstatic ALLOC: PianoAllocator<MyAlloc>\n = PianoAllocator::new(MyAlloc);\n");
let result = inj.apply(source);
assert!(result.contains("PianoAllocator<MyAlloc>"));
assert!(result.contains("fn main()"));
}
#[test]
fn insert_and_replace_in_same_pass() {
let source = "#[global_allocator]\nstatic A: M = M;\n\nfn work() {\n 1\n}\n";
let mut inj = StringInjector::new();
let alloc_end = source.find(";\n").unwrap() + 2;
inj.replace(
0,
alloc_end,
"#[global_allocator]\nstatic A: P<M> = P::new(M);\n",
);
let brace = source.find("{\n 1").unwrap();
inj.insert(brace + 1, " let _g = enter(0);");
let result = inj.apply(source);
assert!(result.contains("P<M>"));
assert!(result.contains("let _g = enter(0);"));
}
}