Skip to main content

objects/object/
blob.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Blob storage for file contents.
3
4use serde::{Deserialize, Serialize};
5
6use super::ContentHash;
7
8/// A blob stores raw file contents.
9///
10/// Blobs are content-addressed: the hash is computed from the content itself.
11/// Two files with identical content will share the same blob.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Blob {
14    content: Vec<u8>,
15}
16
17impl Blob {
18    /// Create a new blob from content.
19    pub fn new(content: Vec<u8>) -> Self {
20        Self { content }
21    }
22
23    /// Create a blob from a byte slice.
24    pub fn from_slice(content: &[u8]) -> Self {
25        Self {
26            content: content.to_vec(),
27        }
28    }
29
30    /// Get the content.
31    pub fn content(&self) -> &[u8] {
32        &self.content
33    }
34
35    /// Get the content as a string (if valid UTF-8).
36    pub fn content_str(&self) -> Option<&str> {
37        std::str::from_utf8(&self.content).ok()
38    }
39
40    /// Consume the blob and return the content.
41    pub fn into_content(self) -> Vec<u8> {
42        self.content
43    }
44
45    /// Compute the content hash for this blob.
46    pub fn hash(&self) -> ContentHash {
47        ContentHash::compute_typed("blob", &self.content)
48    }
49
50    /// Get the size in bytes.
51    pub fn size(&self) -> usize {
52        self.content.len()
53    }
54
55    /// Check if the blob is empty.
56    pub fn is_empty(&self) -> bool {
57        self.content.is_empty()
58    }
59}
60
61impl From<Vec<u8>> for Blob {
62    fn from(content: Vec<u8>) -> Self {
63        Self::new(content)
64    }
65}
66
67impl From<&[u8]> for Blob {
68    fn from(content: &[u8]) -> Self {
69        Self::from_slice(content)
70    }
71}
72
73impl From<String> for Blob {
74    fn from(content: String) -> Self {
75        Self::new(content.into_bytes())
76    }
77}
78
79impl From<&str> for Blob {
80    fn from(content: &str) -> Self {
81        Self::new(content.as_bytes().to_vec())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_blob_creation() {
91        let blob = Blob::new(b"hello world".to_vec());
92        assert_eq!(blob.content(), b"hello world");
93        assert_eq!(blob.size(), 11);
94        assert!(!blob.is_empty());
95    }
96
97    #[test]
98    fn test_blob_hash_deterministic() {
99        let blob1 = Blob::from("hello");
100        let blob2 = Blob::from("hello");
101        assert_eq!(blob1.hash(), blob2.hash());
102    }
103
104    #[test]
105    fn test_blob_hash_differs_for_different_content() {
106        let blob1 = Blob::from("hello");
107        let blob2 = Blob::from("world");
108        assert_ne!(blob1.hash(), blob2.hash());
109    }
110
111    #[test]
112    fn test_blob_content_str() {
113        let blob = Blob::from("hello");
114        assert_eq!(blob.content_str(), Some("hello"));
115
116        let binary_blob = Blob::new(vec![0xff, 0xfe]);
117        assert_eq!(binary_blob.content_str(), None);
118    }
119
120    #[test]
121    fn test_empty_blob() {
122        let blob = Blob::new(vec![]);
123        assert!(blob.is_empty());
124        assert_eq!(blob.size(), 0);
125    }
126}