vtcode_commons/
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!("Input path is not a file: {}", path.display()));
29 }
30
31 if metadata.len() > MAX_INPUT_FILE_BYTES {
32 return Err(anyhow::anyhow!(
33 "Input file too large: {} bytes (max {} bytes)",
34 metadata.len(),
35 MAX_INPUT_FILE_BYTES
36 ));
37 }
38
39 let file_contents = tokio::fs::read(path)
40 .await
41 .with_context(|| format!("Failed to read input file: {}", path.display()))?;
42
43 let filename = path
44 .file_name()
45 .and_then(|name| name.to_str())
46 .filter(|name| !name.is_empty())
47 .map(ToOwned::to_owned)
48 .unwrap_or_else(|| path.display().to_string());
49
50 Ok(FileInputData {
51 base64_data: base64::engine::general_purpose::STANDARD.encode(&file_contents),
52 filename,
53 file_path: path.display().to_string(),
54 size: file_contents.len() as u64,
55 })
56}
57
58pub fn decoded_base64_size(file_data: &str) -> Result<u64> {
59 let payload = inline_base64_payload(file_data);
60 let decoded = base64::engine::general_purpose::STANDARD
61 .decode(payload)
62 .context("Invalid base64 file_data payload")?;
63 Ok(decoded.len() as u64)
64}
65
66fn inline_base64_payload(file_data: &str) -> &str {
67 let trimmed = file_data.trim();
68 if let Some((prefix, payload)) = trimmed.split_once(',')
69 && prefix.contains(";base64")
70 {
71 payload.trim()
72 } else {
73 trimmed
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::{MAX_INPUT_FILE_BYTES, decoded_base64_size};
80
81 #[test]
82 fn decoded_base64_size_supports_raw_base64() {
83 assert_eq!(decoded_base64_size("aGVsbG8=").unwrap(), 5);
84 }
85
86 #[test]
87 fn decoded_base64_size_supports_data_url_prefix() {
88 assert_eq!(decoded_base64_size("data:application/pdf;base64,aGVsbG8=").unwrap(), 5);
89 }
90
91 #[test]
92 fn max_input_file_bytes_matches_openai_limit() {
93 assert_eq!(MAX_INPUT_FILE_BYTES, 50 * 1024 * 1024);
94 }
95}