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