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}