Skip to main content

qubit_fs/read/
read_options.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Read operation options.
9
10use crate::error::FsError;
11use crate::error::FsErrorKind;
12use crate::error::FsOperation;
13use crate::metadata::FileSystemCapabilities;
14use crate::metadata::FileSystemCapability;
15use crate::metadata::ResourceVersion;
16use crate::read::ChecksumPolicy;
17
18/// Options controlling a read operation.
19///
20/// # Examples
21///
22/// ```
23/// use qubit_fs::read::ReadOptions;
24///
25/// let options = ReadOptions::default().with_length(Some(1_024));
26/// assert_eq!(Some(1_024), options.length());
27/// assert!(options.validate().is_ok());
28/// ```
29#[non_exhaustive]
30#[derive(Clone, Debug, Default, Eq, PartialEq)]
31pub struct ReadOptions {
32    /// Optional byte offset.
33    offset: Option<u64>,
34    /// Optional byte length.
35    length: Option<u64>,
36    /// Optional required ETag or provider version.
37    if_match: Option<ResourceVersion>,
38    /// Optional ETag or provider version that must not match.
39    if_none_match: Option<ResourceVersion>,
40    /// Checksum validation policy.
41    checksum: ChecksumPolicy,
42}
43
44impl ReadOptions {
45    /// Returns a copy with the byte offset replaced.
46    #[inline]
47    #[must_use]
48    pub const fn with_offset(mut self, offset: Option<u64>) -> Self {
49        self.offset = offset;
50        self
51    }
52
53    /// Returns the optional byte offset.
54    #[inline]
55    #[must_use]
56    pub const fn offset(&self) -> Option<u64> {
57        self.offset
58    }
59
60    /// Returns a copy with the byte length replaced.
61    #[inline]
62    #[must_use]
63    pub const fn with_length(mut self, length: Option<u64>) -> Self {
64        self.length = length;
65        self
66    }
67
68    /// Returns the optional byte length.
69    #[inline]
70    #[must_use]
71    pub const fn length(&self) -> Option<u64> {
72        self.length
73    }
74
75    /// Returns a copy with the positive version precondition replaced.
76    #[inline]
77    #[must_use]
78    pub fn with_if_match(mut self, if_match: Option<ResourceVersion>) -> Self {
79        self.if_match = if_match;
80        self
81    }
82
83    /// Returns the optional positive version precondition.
84    #[inline]
85    #[must_use]
86    pub const fn if_match(&self) -> Option<&ResourceVersion> {
87        self.if_match.as_ref()
88    }
89
90    /// Returns a copy with the negative version precondition replaced.
91    #[inline]
92    #[must_use]
93    pub fn with_if_none_match(mut self, if_none_match: Option<ResourceVersion>) -> Self {
94        self.if_none_match = if_none_match;
95        self
96    }
97
98    /// Returns the optional negative version precondition.
99    #[inline]
100    #[must_use]
101    pub const fn if_none_match(&self) -> Option<&ResourceVersion> {
102        self.if_none_match.as_ref()
103    }
104
105    /// Returns a copy with the checksum policy replaced.
106    #[inline]
107    #[must_use]
108    pub const fn with_checksum(mut self, checksum: ChecksumPolicy) -> Self {
109        self.checksum = checksum;
110        self
111    }
112
113    /// Returns the checksum policy.
114    #[inline]
115    #[must_use]
116    pub const fn checksum(&self) -> ChecksumPolicy {
117        self.checksum
118    }
119
120    /// Validates provider-independent read options without performing I/O.
121    ///
122    /// A zero-length window is valid, including at the largest offset. Resource
123    /// existence and access permissions are still checked when opening it.
124    /// Offsets at or beyond EOF produce an empty window; lengths extending past
125    /// EOF are truncated. These windows do not imply a resource snapshot.
126    ///
127    /// # Errors
128    /// Returns `InvalidOptions` for mutually exclusive conditions or an
129    /// explicit range whose exclusive end cannot be represented as `u64`.
130    pub fn validate(&self) -> Result<(), FsError> {
131        if self.if_match.is_some() && self.if_none_match.is_some() {
132            return Err(FsError::new(
133                FsErrorKind::InvalidOptions,
134                FsOperation::OpenReader,
135                "if_match and if_none_match cannot both be specified",
136            ));
137        }
138        if let Some(length) = self.length
139            && self.offset.unwrap_or(0).checked_add(length).is_none()
140        {
141            return Err(FsError::new(
142                FsErrorKind::InvalidOptions,
143                FsOperation::OpenReader,
144                "read range exceeds the representable byte offset",
145            ));
146        }
147        Ok(())
148    }
149
150    /// Validates required read semantics against configured capabilities.
151    ///
152    /// Providers should call this method before opening a reader or producing
153    /// any observable side effect.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`FsErrorKind::InvalidOptions`] for mutually exclusive version
158    /// conditions, or [`FsErrorKind::RequirementNotMet`] with the exact
159    /// missing capability for range, conditional, or required-checksum reads.
160    pub fn validate_against(&self, capabilities: FileSystemCapabilities) -> Result<(), FsError> {
161        self.validate()?;
162        if (self.offset.is_some() || self.length.is_some()) && !capabilities.supports(FileSystemCapability::RangeRead) {
163            return Err(missing_requirement(
164                FileSystemCapability::RangeRead,
165                "byte-range reads are required but not supported",
166            ));
167        }
168        if (self.if_match.is_some() || self.if_none_match.is_some())
169            && !capabilities.supports(FileSystemCapability::ConditionalRead)
170        {
171            return Err(missing_requirement(
172                FileSystemCapability::ConditionalRead,
173                "conditional reads are required but not supported",
174            ));
175        }
176        if self.checksum == ChecksumPolicy::Required && !capabilities.supports(FileSystemCapability::ChecksumValidation)
177        {
178            return Err(missing_requirement(
179                FileSystemCapability::ChecksumValidation,
180                "checksum validation is required but not supported",
181            ));
182        }
183        Ok(())
184    }
185}
186
187/// Builds a typed unmet read requirement.
188fn missing_requirement(capability: FileSystemCapability, message: &str) -> FsError {
189    FsError::new(FsErrorKind::RequirementNotMet, FsOperation::OpenReader, message).with_required_capability(capability)
190}
191
192#[cfg(test)]
193mod tests {
194    use std::hint::black_box;
195
196    use super::ReadOptions;
197    use crate::metadata::ResourceVersion;
198
199    #[test]
200    fn version_accessor_is_executed_at_runtime() {
201        let if_match: for<'a> fn(&'a ReadOptions) -> Option<&'a ResourceVersion> = black_box(ReadOptions::if_match);
202        let options = ReadOptions::default().with_if_match(Some(ResourceVersion::new("v1")));
203
204        assert_eq!(Some("v1"), if_match(&options).map(ResourceVersion::as_str));
205    }
206}