summaryrefslogtreecommitdiff
path: root/src/chunkmeta.rs
blob: 9a435fefd48ae169800951b9f16e0e31c368c81c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
//! Metadata about a chunk.

use crate::checksummer::Checksum;
use serde::{Deserialize, Serialize};
use std::default::Default;
use std::str::FromStr;

/// Metadata about chunks.
///
/// We manage three bits of metadata about chunks, in addition to its
/// identifier:
///
/// * for all chunks, a [SHA256][] checksum of the chunk content; we
///   expose this to the server as the chunk "label"
///
/// * for generation chunks, an indication that it is a generation
///   chunk, and a timestamp for when making the generation snapshot
///   ended
///
/// There is no syntax or semantics imposed on the timestamp, but a
/// client should probably use [ISO 8601][] representation.
///
/// For HTTP, the metadata will be serialised as a JSON object, like this:
///
/// ~~~json
/// {
///     "label": "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b",
///     "generation": true,
///     "ended": "2020-09-17T08:17:13+03:00"
/// }
/// ~~~
///
/// This module provides functions for serializing to and from JSON.
/// The JSON doesn't have to include the fields for generations if
/// they're not needed, although when serialized, they will always be
/// there.
///
/// After chunk metadata is created, it is immutable.
///
/// [ISO 8601]: https://en.wikipedia.org/wiki/ISO_8601
/// [SHA256]: https://en.wikipedia.org/wiki/SHA-2
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct ChunkMeta {
    label: String,
    // The remaining fields are Options so that JSON parsing doesn't
    // insist on them being there in the textual representation.
    generation: Option<bool>,
    ended: Option<String>,
}

impl ChunkMeta {
    /// Create a new data chunk.
    ///
    /// Data chunks are not for generations.
    pub fn new(checksum: &Checksum) -> Self {
        ChunkMeta {
            label: checksum.to_string(),
            generation: None,
            ended: None,
        }
    }

    /// Create a new generation chunk.
    pub fn new_generation(checksum: &Checksum, ended: &str) -> Self {
        ChunkMeta {
            label: checksum.to_string(),
            generation: Some(true),
            ended: Some(ended.to_string()),
        }
    }

    /// Is this a generation chunk?
    pub fn is_generation(&self) -> bool {
        matches!(self.generation, Some(true))
    }

    /// When did this generation end?
    pub fn ended(&self) -> Option<&str> {
        self.ended.as_deref()
    }

    /// The label of the content of the chunk.
    ///
    /// The caller should not interpret the label in any way. It
    /// happens to be a SHA256 of the cleartext contents of the
    /// checksum for now, but that _will_ change in the future.
    pub fn label(&self) -> &str {
        &self.label
    }

    /// Serialize from a textual JSON representation.
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
        serde_json::from_str(json)
    }

    /// Serialize as JSON.
    pub fn to_json(&self) -> String {
        serde_json::to_string(self).unwrap()
    }

    /// Serialize as JSON, as a byte vector.
    pub fn to_json_vec(&self) -> Vec<u8> {
        self.to_json().as_bytes().to_vec()
    }
}

impl FromStr for ChunkMeta {
    type Err = serde_json::error::Error;

    /// Parse a JSON representation metadata.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        serde_json::from_str(s)
    }
}

#[cfg(test)]
mod test {
    use super::{Checksum, ChunkMeta};

    #[test]
    fn new_creates_data_chunk() {
        let sum = Checksum::sha256_from_str_unchecked("abcdef");
        let meta = ChunkMeta::new(&sum);
        assert!(!meta.is_generation());
        assert_eq!(meta.ended(), None);
        assert_eq!(meta.label(), "abcdef");
    }

    #[test]
    fn new_generation_creates_generation_chunk() {
        let sum = Checksum::sha256_from_str_unchecked("abcdef");
        let meta = ChunkMeta::new_generation(&sum, "2020-09-17T08:17:13+03:00");
        assert!(meta.is_generation());
        assert_eq!(meta.ended(), Some("2020-09-17T08:17:13+03:00"));
        assert_eq!(meta.label(), "abcdef");
    }

    #[test]
    fn data_chunk_from_json() {
        let meta: ChunkMeta = r#"{"label": "abcdef"}"#.parse().unwrap();
        assert!(!meta.is_generation());
        assert_eq!(meta.ended(), None);
        assert_eq!(meta.label(), "abcdef");
    }

    #[test]
    fn generation_chunk_from_json() {
        let meta: ChunkMeta =
            r#"{"label": "abcdef", "generation": true, "ended": "2020-09-17T08:17:13+03:00"}"#
                .parse()
                .unwrap();
        assert!(meta.is_generation());
        assert_eq!(meta.ended(), Some("2020-09-17T08:17:13+03:00"));
        assert_eq!(meta.label(), "abcdef");
    }

    #[test]
    fn generation_json_roundtrip() {
        let sum = Checksum::sha256_from_str_unchecked("abcdef");
        let meta = ChunkMeta::new_generation(&sum, "2020-09-17T08:17:13+03:00");
        let json = serde_json::to_string(&meta).unwrap();
        let meta2 = serde_json::from_str(&json).unwrap();
        assert_eq!(meta, meta2);
    }

    #[test]
    fn data_json_roundtrip() {
        let sum = Checksum::sha256_from_str_unchecked("abcdef");
        let meta = ChunkMeta::new(&sum);
        let json = meta.to_json_vec();
        let meta2 = serde_json::from_slice(&json).unwrap();
        assert_eq!(meta, meta2);
        assert_eq!(meta.to_json_vec(), meta2.to_json_vec());
    }
}