vtcode_core/utils/
file_input.rs1use anyhow::{Context, Result};
4use base64::Engine as _;
5use std::path::Path;
6
7pub const MAX_INPUT_FILE_BYTES: u64 = 50 * 1024 * 1024;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct FileInputData {
12 pub base64_data: String,
13 pub filename: String,
14 pub file_path: String,
15 pub size: u64,
16}
17
18pub async fn read_input_file_any_path<P: AsRef<Path>>(file_path: P) -> Result<FileInputData> {
22 let path = file_path.as_ref();
23 let metadata = tokio::fs::metadata(path)
24 .await
25 .with_context(|| format!("Failed to stat input file: {}", path.display()))?;
26
27 if !metadata.is_file() {
28 return Err(anyhow::anyhow!(
29 "Input path is not a file: {}",
30 path.display()
31 ));
32 }
33
34 if metadata.len() > MAX_INPUT_FILE_BYTES {
35 return Err(anyhow::anyhow!(
36 "Input file too large: {} bytes (max {} bytes)",
37 metadata.len(),
38 MAX_INPUT_FILE_BYTES
39 ));
40 }
41
42 let file_contents = tokio::fs::read(path)
43 .await
44 .with_context(|| format!("Failed to read input file: {}", path.display()))?;
45
46 let filename = path
47 .file_name()
48 .and_then(|name| name.to_str())
49 .filter(|name| !name.is_empty())
50 .map(ToOwned::to_owned)
51 .unwrap_or_else(|| path.display().to_string());
52
53 Ok(FileInputData {
54 base64_data: base64::engine::general_purpose::STANDARD.encode(&file_contents),
55 filename,
56 file_path: path.display().to_string(),
57 size: file_contents.len() as u64,
58 })
59}
60
61pub fn decoded_base64_size(file_data: &str) -> Result<u64> {
62 let payload = inline_base64_payload(file_data);
63 let decoded = base64::engine::general_purpose::STANDARD
64 .decode(payload)
65 .context("Invalid base64 file_data payload")?;
66 Ok(decoded.len() as u64)
67}
68
69fn inline_base64_payload(file_data: &str) -> &str {
70 let trimmed = file_data.trim();
71 if let Some((prefix, payload)) = trimmed.split_once(',')
72 && prefix.contains(";base64")
73 {
74 payload.trim()
75 } else {
76 trimmed
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::{MAX_INPUT_FILE_BYTES, decoded_base64_size};
83
84 #[test]
85 fn decoded_base64_size_supports_raw_base64() {
86 assert_eq!(decoded_base64_size("aGVsbG8=").unwrap(), 5);
87 }
88
89 #[test]
90 fn decoded_base64_size_supports_data_url_prefix() {
91 assert_eq!(
92 decoded_base64_size("data:application/pdf;base64,aGVsbG8=").unwrap(),
93 5
94 );
95 }
96
97 #[test]
98 fn max_input_file_bytes_matches_openai_limit() {
99 assert_eq!(MAX_INPUT_FILE_BYTES, 50 * 1024 * 1024);
100 }
101}