1use std::path::{Path, PathBuf};
8
9pub fn collect_files(path: &Path) -> Vec<PathBuf> {
33 use ignore::WalkBuilder;
34
35 let mut files = Vec::new();
36 if path.is_file() {
37 files.push(path.to_path_buf());
38 } else if path.is_dir() {
39 let walker = WalkBuilder::new(path)
40 .standard_filters(true) .build();
42
43 for result in walker.flatten() {
44 if result.file_type().is_some_and(|ft| ft.is_file()) {
45 files.push(result.path().to_path_buf());
46 }
47 }
48 }
49 files
50}
51
52pub fn byte_to_line_col(src: &str, byte_idx: usize) -> (usize, usize) {
76 let mut line = 1;
77 let mut col = 1;
78 for (i, ch) in src.char_indices() {
79 if i == byte_idx {
80 return (line, col);
81 }
82 if ch == '\n' {
83 line += 1;
84 col = 1;
85 } else {
86 col += 1;
87 }
88 }
89 (line, col)
90}
91
92pub fn is_safe_path(path: &Path, base_dir: &Path) -> bool {
121 let resolved = if path.is_absolute() {
122 path.to_path_buf()
123 } else {
124 base_dir.join(path)
125 };
126
127 if let (Ok(resolved_canon), Ok(base_canon)) = (resolved.canonicalize(), base_dir.canonicalize())
129 {
130 return resolved_canon.starts_with(&base_canon);
131 }
132
133 if path.is_absolute() {
137 if let Ok(base_canon) = base_dir.canonicalize() {
138 return resolved.starts_with(&base_canon);
139 }
140 return false;
141 }
142
143 let mut depth: i32 = 0;
145 for component in path.components() {
146 match component {
147 std::path::Component::ParentDir => depth -= 1,
148 std::path::Component::Normal(_) => depth += 1,
149 _ => {}
150 }
151 if depth < 0 {
152 return false; }
154 }
155 true }
157
158pub fn base64_encode(data: &[u8]) -> String {
171 use base64::{engine::general_purpose::STANDARD, Engine};
172 STANDARD.encode(data)
173}
174
175pub fn base64_decode(s: &str) -> Vec<u8> {
187 use base64::{engine::general_purpose::STANDARD, Engine};
188 STANDARD.decode(s).unwrap_or_default()
189}
190
191pub const DEFAULT_PAGE_SIZE: usize = 100;
197
198#[derive(Debug, Clone)]
200pub struct PaginatedResult<T> {
201 pub items: Vec<T>,
203 pub next_cursor: Option<String>,
205}
206
207impl<T> PaginatedResult<T> {
208 pub fn new(items: Vec<T>, next_cursor: Option<String>) -> Self {
210 Self { items, next_cursor }
211 }
212
213 pub fn empty() -> Self {
215 Self {
216 items: Vec::new(),
217 next_cursor: None,
218 }
219 }
220}
221
222pub fn encode_cursor(offset: usize) -> String {
228 base64_encode(offset.to_string().as_bytes())
229}
230
231pub fn decode_cursor(cursor: &str) -> Option<usize> {
236 let bytes = base64_decode(cursor);
237 let s = String::from_utf8(bytes).ok()?;
238 s.parse().ok()
239}
240
241pub fn paginate<T: Clone>(
256 items: &[T],
257 cursor: Option<&str>,
258 page_size: usize,
259) -> PaginatedResult<T> {
260 if items.is_empty() {
261 return PaginatedResult::empty();
262 }
263
264 let start = cursor.and_then(decode_cursor).unwrap_or(0).min(items.len());
266
267 let end = (start + page_size).min(items.len());
268 let page_items: Vec<T> = items[start..end].to_vec();
269
270 let next_cursor = if end < items.len() {
272 Some(encode_cursor(end))
273 } else {
274 None
275 };
276
277 PaginatedResult::new(page_items, next_cursor)
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
287 fn test_is_safe_path_relative() {
288 let base = std::env::current_dir().unwrap();
289 assert!(is_safe_path(Path::new("foo"), &base));
290 assert!(is_safe_path(Path::new("foo/bar"), &base));
291 assert!(is_safe_path(Path::new("./foo"), &base));
292 }
293
294 #[test]
295 fn test_is_safe_path_parent_escape() {
296 let base = std::env::current_dir().unwrap();
297 assert!(!is_safe_path(Path::new("../foo"), &base));
298 assert!(!is_safe_path(Path::new("foo/../../bar"), &base));
299 assert!(!is_safe_path(Path::new(".."), &base));
300 }
301
302 #[test]
303 fn test_is_safe_path_absolute_outside() {
304 let base = std::env::current_dir().unwrap();
305 assert!(!is_safe_path(Path::new("/tmp"), &base));
306 assert!(!is_safe_path(Path::new("/etc/passwd"), &base));
307 assert!(!is_safe_path(Path::new("/home"), &base));
308 }
309
310 #[test]
313 fn test_base64_roundtrip() {
314 let original = "42";
315 let encoded = base64_encode(original.as_bytes());
316 let decoded = String::from_utf8(base64_decode(&encoded)).unwrap();
317 assert_eq!(original, decoded);
318 }
319
320 #[test]
321 fn test_base64_offset_encoding() {
322 let offsets = [0, 10, 100, 12345];
323 for offset in offsets {
324 let encoded = base64_encode(offset.to_string().as_bytes());
325 let decoded: usize = String::from_utf8(base64_decode(&encoded))
326 .unwrap()
327 .parse()
328 .unwrap();
329 assert_eq!(offset, decoded);
330 }
331 }
332
333 #[test]
334 fn test_base64_invalid_input() {
335 let result = base64_decode("!!!invalid!!!");
337 assert!(result.is_empty());
338 }
339
340 #[test]
343 fn test_byte_to_line_col_start() {
344 let src = "hello\nworld\n";
345 let (line, col) = byte_to_line_col(src, 0);
346 assert_eq!((line, col), (1, 1));
347 }
348
349 #[test]
350 fn test_byte_to_line_col_middle_first_line() {
351 let src = "hello\nworld\n";
352 let (line, col) = byte_to_line_col(src, 2);
353 assert_eq!((line, col), (1, 3)); }
355
356 #[test]
357 fn test_byte_to_line_col_second_line() {
358 let src = "hello\nworld\n";
359 let (line, col) = byte_to_line_col(src, 6);
360 assert_eq!((line, col), (2, 1)); }
362
363 #[test]
364 fn test_byte_to_line_col_end() {
365 let src = "hello\nworld\n";
366 let (line, col) = byte_to_line_col(src, 11);
367 assert_eq!((line, col), (2, 6)); }
369
370 #[test]
371 fn test_byte_to_line_col_beyond_end() {
372 let src = "hi";
373 let (line, col) = byte_to_line_col(src, 100);
374 assert_eq!((line, col), (1, 3));
376 }
377
378 #[test]
381 fn test_encode_decode_cursor() {
382 let cursor = encode_cursor(42);
383 assert_eq!(decode_cursor(&cursor), Some(42));
384 }
385
386 #[test]
387 fn test_decode_invalid_cursor() {
388 assert_eq!(decode_cursor("invalid"), None);
389 assert_eq!(decode_cursor("!!!"), None);
390 }
391
392 #[test]
393 fn test_paginate_empty() {
394 let items: Vec<i32> = vec![];
395 let result = paginate(&items, None, 10);
396 assert!(result.items.is_empty());
397 assert!(result.next_cursor.is_none());
398 }
399
400 #[test]
401 fn test_paginate_no_cursor_within_limit() {
402 let items: Vec<i32> = vec![1, 2, 3, 4, 5];
403 let result = paginate(&items, None, 10);
404 assert_eq!(result.items, vec![1, 2, 3, 4, 5]);
405 assert!(result.next_cursor.is_none());
406 }
407
408 #[test]
409 fn test_paginate_no_cursor_exceeds_limit() {
410 let items: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
411 let result = paginate(&items, None, 3);
412 assert_eq!(result.items, vec![1, 2, 3]);
413 assert!(result.next_cursor.is_some());
414 }
415
416 #[test]
417 fn test_paginate_with_cursor() {
418 let items: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
419
420 let result1 = paginate(&items, None, 3);
422 assert_eq!(result1.items, vec![1, 2, 3]);
423
424 let result2 = paginate(&items, result1.next_cursor.as_deref(), 3);
426 assert_eq!(result2.items, vec![4, 5, 6]);
427
428 let result3 = paginate(&items, result2.next_cursor.as_deref(), 3);
430 assert_eq!(result3.items, vec![7, 8, 9]);
431
432 let result4 = paginate(&items, result3.next_cursor.as_deref(), 3);
434 assert_eq!(result4.items, vec![10]);
435 assert!(result4.next_cursor.is_none());
436 }
437
438 #[test]
439 fn test_paginate_invalid_cursor() {
440 let items: Vec<i32> = vec![1, 2, 3, 4, 5];
441 let result = paginate(&items, Some("invalid"), 10);
443 assert_eq!(result.items, vec![1, 2, 3, 4, 5]);
444 }
445}