Skip to main content

agents_kit/
file.rs

1use std::path::{Path, PathBuf};
2
3use crate::{KitError, WorkspaceContext};
4
5/// Exact content returned by a portable workspace-relative file read.
6///
7/// # Examples
8///
9/// ```
10/// use std::path::{Path, PathBuf};
11///
12/// use agents_kit::conversation::FileRead;
13///
14/// let file = FileRead::from_bytes(
15///     PathBuf::from("notes.txt"),
16///     b"ready\n".to_vec(),
17/// )?;
18/// assert_eq!(file.path(), Path::new("notes.txt"));
19/// assert_eq!(file.text(), Some("ready\n"));
20/// assert_eq!(file.bytes(), None);
21/// # Ok::<(), agents_kit::KitError>(())
22/// ```
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct FileRead {
25    path: PathBuf,
26    content: FileReadContent,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
30enum FileReadContent {
31    Text(String),
32    Binary { bytes: Vec<u8>, mime_type: String },
33}
34
35impl FileRead {
36    /// Validates the portable relative path and preserves the exact file content.
37    ///
38    /// This constructs a returned value; it does not read the filesystem.
39    /// UTF-8 bytes become text, while invalid UTF-8 remains exact binary data.
40    ///
41    /// # Errors
42    /// Returns [`crate::error::ErrorKind::InvalidInput`] for absolute paths,
43    /// parent traversal, or invalid portable path components.
44    ///
45    /// ```
46    /// use agents_kit::conversation::FileRead;
47    /// let file = FileRead::from_bytes("notes.txt".into(), b"ready".to_vec())?;
48    /// assert_eq!(file.text(), Some("ready"));
49    /// assert!(FileRead::from_bytes("../secret".into(), vec![]).is_err());
50    /// # Ok::<(), agents_kit::KitError>(())
51    /// ```
52    pub fn from_bytes(path: PathBuf, bytes: Vec<u8>) -> Result<Self, KitError> {
53        let path = WorkspaceContext::normalized_relative(&path)?;
54        let content = match String::from_utf8(bytes) {
55            Ok(text) => FileReadContent::Text(text),
56            Err(error) => FileReadContent::Binary {
57                bytes: error.into_bytes(),
58                mime_type: "application/octet-stream".to_owned(),
59            },
60        };
61        Ok(Self { path, content })
62    }
63
64    /// Returns the validated workspace-relative path.
65    ///
66    /// ```
67    /// use agents_kit::conversation::FileRead;
68    /// let file = FileRead::from_bytes("notes.txt".into(), vec![])?;
69    /// assert_eq!(file.path(), std::path::Path::new("notes.txt"));
70    /// # Ok::<(), agents_kit::KitError>(())
71    /// ```
72    pub fn path(&self) -> &Path {
73        &self.path
74    }
75
76    /// Returns the exact text when the file bytes are valid UTF-8.
77    ///
78    /// ```
79    /// use agents_kit::conversation::FileRead;
80    /// let file = FileRead::from_bytes("notes.txt".into(), b"ready\n".to_vec())?;
81    /// assert_eq!(file.text(), Some("ready\n"));
82    /// # Ok::<(), agents_kit::KitError>(())
83    /// ```
84    pub fn text(&self) -> Option<&str> {
85        match &self.content {
86            FileReadContent::Text(text) => Some(text),
87            FileReadContent::Binary { .. } => None,
88        }
89    }
90
91    /// Returns the exact bytes when the file content is not valid UTF-8.
92    ///
93    /// ```
94    /// use agents_kit::conversation::FileRead;
95    /// let file = FileRead::from_bytes("data.bin".into(), vec![0xff, 0x00])?;
96    /// assert_eq!(file.bytes(), Some([0xff, 0x00].as_slice()));
97    /// assert_eq!(file.text(), None);
98    /// # Ok::<(), agents_kit::KitError>(())
99    /// ```
100    pub fn bytes(&self) -> Option<&[u8]> {
101        match &self.content {
102            FileReadContent::Text(_) => None,
103            FileReadContent::Binary { bytes, .. } => Some(bytes),
104        }
105    }
106
107    /// Returns the MIME type when the file content is not valid UTF-8.
108    ///
109    /// ```
110    /// use agents_kit::conversation::FileRead;
111    /// let file = FileRead::from_bytes("data.bin".into(), vec![0xff])?;
112    /// assert_eq!(file.mime_type(), Some("application/octet-stream"));
113    /// # Ok::<(), agents_kit::KitError>(())
114    /// ```
115    pub fn mime_type(&self) -> Option<&str> {
116        match &self.content {
117            FileReadContent::Text(_) => None,
118            FileReadContent::Binary { mime_type, .. } => Some(mime_type),
119        }
120    }
121}