radicle_source/object/
blob.rs1use std::{
19 convert::TryFrom as _,
20 str::{self, FromStr as _},
21};
22
23use serde::{
24 ser::{SerializeStruct as _, Serializer},
25 Serialize,
26};
27
28use radicle_surf::{
29 file_system,
30 vcs::git::{Browser, Rev},
31};
32
33use crate::{
34 commit,
35 error::Error,
36 object::{Info, ObjectType},
37 revision::Revision,
38};
39
40#[cfg(feature = "syntax")]
41use crate::syntax;
42
43pub struct Blob {
45 pub content: BlobContent,
47 pub info: Info,
49 pub path: String,
51}
52
53impl Blob {
54 #[must_use]
56 pub fn is_binary(&self) -> bool {
57 matches!(self.content, BlobContent::Binary(_))
58 }
59
60 #[must_use]
62 pub const fn is_html(&self) -> bool {
63 matches!(self.content, BlobContent::Html(_))
64 }
65}
66
67impl Serialize for Blob {
68 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
69 where
70 S: Serializer,
71 {
72 let mut state = serializer.serialize_struct("Blob", 5)?;
73 state.serialize_field("binary", &self.is_binary())?;
74 state.serialize_field("html", &self.is_html())?;
75 state.serialize_field("content", &self.content)?;
76 state.serialize_field("info", &self.info)?;
77 state.serialize_field("path", &self.path)?;
78 state.end()
79 }
80}
81
82#[derive(PartialEq)]
84pub enum BlobContent {
85 Plain(String),
87 Html(String),
93 Binary(Vec<u8>),
95}
96
97impl Serialize for BlobContent {
98 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
99 where
100 S: Serializer,
101 {
102 match self {
103 Self::Plain(content) | Self::Html(content) => serializer.serialize_str(content),
104 Self::Binary(bytes) => {
105 let encoded = base64::encode(bytes);
106 serializer.serialize_str(&encoded)
107 },
108 }
109 }
110}
111
112pub fn blob<P>(
119 browser: &mut Browser,
120 maybe_revision: Option<Revision<P>>,
121 path: &str,
122) -> Result<Blob, Error>
123where
124 P: ToString,
125{
126 make_blob(browser, maybe_revision, path, content)
127}
128
129fn make_blob<P, C>(
130 browser: &mut Browser,
131 maybe_revision: Option<Revision<P>>,
132 path: &str,
133 content: C,
134) -> Result<Blob, Error>
135where
136 P: ToString,
137 C: FnOnce(&[u8]) -> BlobContent,
138{
139 let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
140 if let Some(revision) = maybe_revision {
141 browser.rev(revision)?;
142 }
143
144 let root = browser.get_directory()?;
145 let p = file_system::Path::from_str(path)?;
146
147 let file = root
148 .find_file(p.clone())
149 .ok_or_else(|| Error::PathNotFound(p.clone()))?;
150
151 let mut commit_path = file_system::Path::root();
152 commit_path.append(p.clone());
153
154 let last_commit = browser
155 .last_commit(commit_path)?
156 .map(|c| commit::Header::from(&c));
157 let (_rest, last) = p.split_last();
158
159 let content = content(&file.contents);
160
161 Ok(Blob {
162 content,
163 info: Info {
164 name: last.to_string(),
165 object_type: ObjectType::Blob,
166 last_commit,
167 },
168 path: path.to_string(),
169 })
170}
171
172fn content(content: &[u8]) -> BlobContent {
174 match str::from_utf8(content) {
175 Ok(utf8) => BlobContent::Plain(utf8.to_owned()),
176 Err(_) => BlobContent::Binary(content.to_owned()),
177 }
178}
179
180#[cfg(feature = "syntax")]
181pub mod highlighting {
182 use super::*;
183
184 pub fn blob<P>(
191 browser: &mut Browser,
192 maybe_revision: Option<Revision<P>>,
193 path: &str,
194 theme: Option<&str>,
195 ) -> Result<Blob, Error>
196 where
197 P: ToString,
198 {
199 make_blob(browser, maybe_revision, path, |contents| {
200 content(path, contents, theme)
201 })
202 }
203
204 fn content(path: &str, content: &[u8], theme_name: Option<&str>) -> BlobContent {
207 let content = match str::from_utf8(content) {
208 Ok(content) => content,
209 Err(_) => return BlobContent::Binary(content.to_owned()),
210 };
211
212 match theme_name {
213 None => BlobContent::Plain(content.to_owned()),
214 Some(theme) => syntax::highlight(path, content, theme)
215 .map_or_else(|| BlobContent::Plain(content.to_owned()), BlobContent::Html),
216 }
217 }
218}