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}