Skip to main content

google_cloud_storage/
model_ext.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Extends [model][crate::model] with types that improve type safety and/or
16//! ergonomics.
17
18use crate::error::KeyAes256Error;
19use base64::{Engine, prelude::BASE64_STANDARD};
20use sha2::{Digest, Sha256};
21
22mod open_object_request;
23pub use open_object_request::OpenObjectRequest;
24
25#[cfg(google_cloud_unstable_storage_bidi)]
26mod open_appendable_object_request;
27#[cfg(google_cloud_unstable_storage_bidi)]
28#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
29pub use open_appendable_object_request::OpenAppendableObjectRequest;
30
31#[cfg(google_cloud_unstable_storage_bidi)]
32mod reopen_appendable_object_request;
33#[cfg(google_cloud_unstable_storage_bidi)]
34#[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
35pub use reopen_appendable_object_request::ReopenAppendableObjectRequest;
36
37/// ObjectHighlights contains select metadata from a [crate::model::Object].
38#[derive(Clone, Debug, Default, PartialEq)]
39#[non_exhaustive]
40pub struct ObjectHighlights {
41    /// The content generation of this object. Used for object versioning.
42    pub generation: i64,
43
44    /// The version of the metadata for this generation of this
45    /// object. Used for preconditions and for detecting changes in metadata. A
46    /// metageneration number is only meaningful in the context of a particular
47    /// generation of a particular object.
48    pub metageneration: i64,
49
50    /// Content-Length of the object data in bytes, matching [RFC 7230 §3.3.2].
51    ///
52    /// [rfc 7230 §3.3.2]: https://tools.ietf.org/html/rfc7230#section-3.3.2
53    pub size: i64,
54
55    /// Content-Encoding of the object data, matching [RFC 7231 §3.1.2.2].
56    ///
57    /// [rfc 7231 §3.1.2.2]: https://tools.ietf.org/html/rfc7231#section-3.1.2.2
58    pub content_encoding: String,
59
60    /// Hashes for the data part of this object. The checksums of the complete
61    /// object regardless of data range. If the object is read in full, the
62    /// client should compute one of these checksums over the read object and
63    /// compare it against the value provided here.
64    pub checksums: std::option::Option<crate::model::ObjectChecksums>,
65
66    /// Storage class of the object.
67    pub storage_class: String,
68
69    /// Content-Language of the object data, matching [RFC 7231 §3.1.3.2].
70    ///
71    /// [rfc 7231 §3.1.3.2]: https://tools.ietf.org/html/rfc7231#section-3.1.3.2
72    pub content_language: String,
73
74    /// Content-Type of the object data, matching [RFC 7231 §3.1.1.5]. If an
75    /// object is stored without a Content-Type, it is served as
76    /// `application/octet-stream`.
77    ///
78    /// [rfc 7231 §3.1.1.5]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5
79    pub content_type: String,
80
81    /// Content-Disposition of the object data, matching [RFC 6266].
82    ///
83    /// [rfc 6266]: https://tools.ietf.org/html/rfc6266
84    pub content_disposition: String,
85
86    /// The etag of the object.
87    pub etag: String,
88
89    /// User-provided metadata, in key/value pairs.
90    ///
91    /// Populated from `x-goog-meta-*` response headers; keys have the
92    /// `x-goog-meta-` prefix stripped and are lowercased by the server.
93    pub metadata: std::collections::HashMap<String, String>,
94}
95
96#[derive(Debug, Clone)]
97/// KeyAes256 represents an AES-256 encryption key used with the
98/// Customer-Supplied Encryption Keys (CSEK) feature.
99///
100/// This key must be exactly 32 bytes in length and should be provided in its
101/// raw (unencoded) byte format.
102///
103/// # Examples
104///
105/// Creating a `KeyAes256` instance from a valid byte slice:
106/// ```
107/// # use google_cloud_storage::{model_ext::KeyAes256, error::KeyAes256Error};
108/// let raw_key_bytes: [u8; 32] = [0x42; 32]; // Example 32-byte key
109/// let key_aes_256 = KeyAes256::new(&raw_key_bytes)?;
110/// # Ok::<(), KeyAes256Error>(())
111/// ```
112///
113/// Handling an error for an invalid key length:
114/// ```
115/// # use google_cloud_storage::{model_ext::KeyAes256, error::KeyAes256Error};
116/// let invalid_key_bytes: &[u8] = b"too_short_key"; // Less than 32 bytes
117/// let result = KeyAes256::new(invalid_key_bytes);
118///
119/// assert!(matches!(result, Err(KeyAes256Error::InvalidLength)));
120/// ```
121pub struct KeyAes256 {
122    key: [u8; 32],
123}
124
125impl KeyAes256 {
126    /// Attempts to create a new [KeyAes256].
127    ///
128    /// This conversion will succeed only if the input slice is exactly 32 bytes long.
129    ///
130    /// # Example
131    /// ```
132    /// # use google_cloud_storage::{model_ext::KeyAes256, error::KeyAes256Error};
133    /// let raw_key_bytes: [u8; 32] = [0x42; 32]; // Example 32-byte key
134    /// let key_aes_256 = KeyAes256::new(&raw_key_bytes)?;
135    /// # Ok::<(), KeyAes256Error>(())
136    /// ```
137    pub fn new(key: &[u8]) -> std::result::Result<Self, KeyAes256Error> {
138        match key.len() {
139            32 => Ok(Self {
140                key: key[..32].try_into().unwrap(),
141            }),
142            _ => Err(KeyAes256Error::InvalidLength),
143        }
144    }
145}
146
147impl std::convert::From<KeyAes256> for crate::model::CommonObjectRequestParams {
148    fn from(value: KeyAes256) -> Self {
149        // sha2::digest::generic_array::GenericArray::<T, N>::as_slice is deprecated.
150        // Our dependencies need to update to generic_array 1.x.
151        // See https://github.com/RustCrypto/traits/issues/2036 for more info.
152        #[allow(deprecated)]
153        crate::model::CommonObjectRequestParams::new()
154            .set_encryption_algorithm("AES256")
155            .set_encryption_key_bytes(value.key.to_vec())
156            .set_encryption_key_sha256_bytes(Sha256::digest(value.key).as_slice().to_owned())
157    }
158}
159
160impl std::fmt::Display for KeyAes256 {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        write!(f, "{}", BASE64_STANDARD.encode(self.key))
163    }
164}
165
166/// Define read ranges for use with [ReadObject].
167///
168/// # Example: read the first 100 bytes of an object
169/// ```
170/// # use google_cloud_storage::client::Storage;
171/// # use google_cloud_storage::model_ext::ReadRange;
172/// # async fn sample(client: &Storage) -> anyhow::Result<()> {
173/// let response = client
174///     .read_object("projects/_/buckets/my-bucket", "my-object")
175///     .set_read_range(ReadRange::head(100))
176///     .send()
177///     .await?;
178/// println!("response details={response:?}");
179/// # Ok(()) }
180/// ```
181///
182/// Cloud Storage supports reading a portion of an object. These portions can
183/// be specified as offsets from the beginning of the object, offsets from the
184/// end of the object, or as ranges with a starting and ending bytes. This type
185/// defines a type-safe interface to represent only valid ranges.
186///
187/// [ReadObject]: crate::builder::storage::ReadObject
188#[derive(Clone, Debug, PartialEq)]
189pub struct ReadRange(pub(crate) RequestedRange);
190
191impl ReadRange {
192    /// Returns a range representing all the bytes in the object.
193    ///
194    /// # Example
195    /// ```
196    /// # use google_cloud_storage::client::Storage;
197    /// # use google_cloud_storage::model_ext::ReadRange;
198    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
199    /// let response = client
200    ///     .read_object("projects/_/buckets/my-bucket", "my-object")
201    ///     .set_read_range(ReadRange::all())
202    ///     .send()
203    ///     .await?;
204    /// println!("response details={response:?}");
205    /// # Ok(()) }
206    pub fn all() -> Self {
207        Self::offset(0)
208    }
209
210    /// Returns a range representing the bytes starting at `offset`.
211    ///
212    /// # Example
213    /// ```
214    /// # use google_cloud_storage::client::Storage;
215    /// # use google_cloud_storage::model_ext::ReadRange;
216    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
217    /// let response = client
218    ///     .read_object("projects/_/buckets/my-bucket", "my-object")
219    ///     .set_read_range(ReadRange::offset(1_000_000))
220    ///     .send()
221    ///     .await?;
222    /// println!("response details={response:?}");
223    /// # Ok(()) }
224    pub fn offset(offset: u64) -> Self {
225        Self(RequestedRange::Offset(offset))
226    }
227
228    /// Returns a range representing the last `count` bytes of the object.
229    ///
230    /// # Example
231    /// ```
232    /// # use google_cloud_storage::client::Storage;
233    /// # use google_cloud_storage::model_ext::ReadRange;
234    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
235    /// let response = client
236    ///     .read_object("projects/_/buckets/my-bucket", "my-object")
237    ///     .set_read_range(ReadRange::tail(100))
238    ///     .send()
239    ///     .await?;
240    /// println!("response details={response:?}");
241    /// # Ok(()) }
242    pub fn tail(count: u64) -> Self {
243        Self(RequestedRange::Tail(count))
244    }
245
246    /// Returns a range representing the first `count` bytes of the object.
247    ///
248    /// # Example
249    /// ```
250    /// # use google_cloud_storage::client::Storage;
251    /// # use google_cloud_storage::model_ext::ReadRange;
252    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
253    /// let response = client
254    ///     .read_object("projects/_/buckets/my-bucket", "my-object")
255    ///     .set_read_range(ReadRange::head(100))
256    ///     .send()
257    ///     .await?;
258    /// println!("response details={response:?}");
259    /// # Ok(()) }
260    pub fn head(count: u64) -> Self {
261        Self::segment(0, count)
262    }
263
264    /// Returns a range representing the `count` bytes starting at `offset`.
265    ///
266    /// # Example
267    /// ```
268    /// # use google_cloud_storage::client::Storage;
269    /// # use google_cloud_storage::model_ext::ReadRange;
270    /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
271    /// let response = client
272    ///     .read_object("projects/_/buckets/my-bucket", "my-object")
273    ///     .set_read_range(ReadRange::segment(1_000_000, 1_000))
274    ///     .send()
275    ///     .await?;
276    /// println!("response details={response:?}");
277    /// # Ok(()) }
278    pub fn segment(offset: u64, count: u64) -> Self {
279        Self(RequestedRange::Segment {
280            offset,
281            limit: count,
282        })
283    }
284}
285
286impl crate::model::ReadObjectRequest {
287    pub(crate) fn with_range(&mut self, range: ReadRange) {
288        // The limit for GCS objects is (currently) 5TiB, and the gRPC protocol
289        // uses i64 for the offset and limit. Clamping the values to the
290        // `[0, i64::MAX]`` range is safe, in that it does not lose any
291        // functionality.
292        match range.0 {
293            RequestedRange::Offset(o) => {
294                self.read_offset = o.clamp(0, i64::MAX as u64) as i64;
295            }
296            RequestedRange::Tail(t) => {
297                // Yes, -i64::MAX is different from i64::MIN, but both are
298                // safe in this context.
299                self.read_offset = -(t.clamp(0, i64::MAX as u64) as i64);
300            }
301            RequestedRange::Segment { offset, limit } => {
302                self.read_offset = offset.clamp(0, i64::MAX as u64) as i64;
303                self.read_limit = limit.clamp(0, i64::MAX as u64) as i64;
304            }
305        }
306    }
307}
308
309#[derive(Clone, Copy, Debug, PartialEq)]
310pub(crate) enum RequestedRange {
311    Offset(u64),
312    Tail(u64),
313    Segment { offset: u64, limit: u64 },
314}
315
316/// Represents the parameters of a [WriteObject] request.
317///
318/// This type is only used in mocks of the `Storage` client.
319///
320/// [WriteObject]: crate::builder::storage::WriteObject
321#[derive(Debug, PartialEq)]
322#[non_exhaustive]
323#[allow(dead_code)]
324pub struct WriteObjectRequest {
325    /// The object attributes and pre-conditions for the write operation.
326    pub spec: crate::model::WriteObjectSpec,
327    /// Additional request parameters that are not part of the object attributes.
328    pub params: Option<crate::model::CommonObjectRequestParams>,
329}
330
331#[cfg(test)]
332pub(crate) mod tests {
333    use super::*;
334    use crate::model::ReadObjectRequest;
335    use base64::{Engine, prelude::BASE64_STANDARD};
336    use test_case::test_case;
337
338    type Result = anyhow::Result<()>;
339
340    /// This is used by the request builder tests.
341    pub(crate) fn create_key_helper() -> (Vec<u8>, String, Vec<u8>, String) {
342        // Make a 32-byte key.
343        let key = vec![b'a'; 32];
344        let key_base64 = BASE64_STANDARD.encode(key.clone());
345
346        let key_sha256 = Sha256::digest(key.clone());
347        let key_sha256_base64 = BASE64_STANDARD.encode(key_sha256);
348        (key, key_base64, key_sha256.to_vec(), key_sha256_base64)
349    }
350
351    #[test]
352    // This tests converting to KeyAes256 from some different types
353    // that can get converted to &[u8].
354    fn test_key_aes_256() -> Result {
355        let v_slice: &[u8] = &[b'c'; 32];
356        KeyAes256::new(v_slice)?;
357
358        let v_vec: Vec<u8> = vec![b'a'; 32];
359        KeyAes256::new(&v_vec)?;
360
361        let v_array: [u8; 32] = [b'a'; 32];
362        KeyAes256::new(&v_array)?;
363
364        let v_bytes: bytes::Bytes = bytes::Bytes::copy_from_slice(&v_array);
365        KeyAes256::new(&v_bytes)?;
366
367        Ok(())
368    }
369
370    #[test_case(&[b'a'; 0]; "no bytes")]
371    #[test_case(&[b'a'; 1]; "not enough bytes")]
372    #[test_case(&[b'a'; 33]; "too many bytes")]
373    fn test_key_aes_256_err(input: &[u8]) {
374        KeyAes256::new(input).unwrap_err();
375    }
376
377    #[test]
378    fn test_key_aes_256_to_control_model_object() -> Result {
379        let (key, _, key_sha256, _) = create_key_helper();
380        let key_aes_256 = KeyAes256::new(&key)?;
381        let params = crate::model::CommonObjectRequestParams::from(key_aes_256);
382        assert_eq!(params.encryption_algorithm, "AES256");
383        assert_eq!(params.encryption_key_bytes, key);
384        assert_eq!(params.encryption_key_sha256_bytes, key_sha256);
385        Ok(())
386    }
387
388    #[test_case(100, 100)]
389    #[test_case(u64::MAX, i64::MAX)]
390    #[test_case(0, 0)]
391    fn apply_offset(input: u64, want: i64) {
392        let range = ReadRange::offset(input);
393        let mut request = ReadObjectRequest::new();
394        request.with_range(range);
395        assert_eq!(request.read_offset, want);
396        assert_eq!(request.read_limit, 0);
397    }
398
399    #[test_case(100, 100)]
400    #[test_case(u64::MAX, i64::MAX)]
401    #[test_case(0, 0)]
402    fn apply_head(input: u64, want: i64) {
403        let range = ReadRange::head(input);
404        let mut request = ReadObjectRequest::new();
405        request.with_range(range);
406        assert_eq!(request.read_offset, 0);
407        assert_eq!(request.read_limit, want);
408    }
409
410    #[test_case(100, -100)]
411    #[test_case(u64::MAX, -i64::MAX)]
412    #[test_case(0, 0)]
413    fn apply_tail(input: u64, want: i64) {
414        let range = ReadRange::tail(input);
415        let mut request = ReadObjectRequest::new();
416        request.with_range(range);
417        assert_eq!(request.read_offset, want);
418        assert_eq!(request.read_limit, 0);
419    }
420
421    #[test_case(100, 100)]
422    #[test_case(u64::MAX, i64::MAX)]
423    #[test_case(0, 0)]
424    fn apply_segment_offset(input: u64, want: i64) {
425        let range = ReadRange::segment(input, 2000);
426        let mut request = ReadObjectRequest::new();
427        request.with_range(range);
428        assert_eq!(request.read_offset, want);
429        assert_eq!(request.read_limit, 2000);
430    }
431
432    #[test_case(100, 100)]
433    #[test_case(u64::MAX, i64::MAX)]
434    #[test_case(0, 0)]
435    fn apply_segment_limit(input: u64, want: i64) {
436        let range = ReadRange::segment(1000, input);
437        let mut request = ReadObjectRequest::new();
438        request.with_range(range);
439        assert_eq!(request.read_offset, 1000);
440        assert_eq!(request.read_limit, want);
441    }
442
443    #[test]
444    fn test_key_aes_256_display() -> Result {
445        let (key, key_base64, _, _) = create_key_helper();
446        let key_aes_256 = KeyAes256::new(&key)?;
447        assert_eq!(key_aes_256.to_string(), key_base64);
448        Ok(())
449    }
450}