radicle_source/object/
blob.rs

1// This file is part of radicle-surf
2// <https://github.com/radicle-dev/radicle-surf>
3//
4// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License version 3 or
8// later as published by the Free Software Foundation.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18use 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
43/// File data abstraction.
44pub struct Blob {
45    /// Actual content of the file, if the content is ASCII.
46    pub content: BlobContent,
47    /// Extra info for the file.
48    pub info: Info,
49    /// Absolute path to the object from the root of the repo.
50    pub path: String,
51}
52
53impl Blob {
54    /// Indicates if the content of the [`Blob`] is binary.
55    #[must_use]
56    pub fn is_binary(&self) -> bool {
57        matches!(self.content, BlobContent::Binary(_))
58    }
59
60    /// Indicates if the content of the [`Blob`] is HTML.
61    #[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/// Variants of blob content.
83#[derive(PartialEq)]
84pub enum BlobContent {
85    /// Content is plain text and can be passed as a string.
86    Plain(String),
87    /// Content is syntax-highlighted HTML.
88    ///
89    /// Note that is necessary to enable the `syntax` feature flag for this
90    /// variant to be constructed. Use `highlighting::blob`, instead of
91    /// [`blob`] to get highlighted content.
92    Html(String),
93    /// Content is binary and needs special treatment.
94    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
112/// Returns the [`Blob`] for a file at `revision` under `path`.
113///
114/// # Errors
115///
116/// Will return [`Error`] if the project doesn't exist or a surf interaction
117/// fails.
118pub 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
172/// Return a [`BlobContent`] given a byte slice.
173fn 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    /// Returns the [`Blob`] for a file at `revision` under `path`.
185    ///
186    /// # Errors
187    ///
188    /// Will return [`Error`] if the project doesn't exist or a surf interaction
189    /// fails.
190    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    /// Return a [`BlobContent`] given a file path, content and theme. Attempts
205    /// to perform syntax highlighting when the theme is `Some`.
206    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}