Skip to main content

typst_pack/opendal/
pack_archive.rs

1//! Pack Archive Read through caller-supplied OpenDAL operators.
2
3use super::BoxError;
4use super::read::{
5    ExactPathReadOperation, ResolvedOperators, exact_path_absent_error, read_exact_path,
6};
7use super::{Location, LocationRoleError, OperatorResolver};
8use crate::PackArchiveBytes;
9use crate::pack_archive::{ReadLimitError, ReadLimits, ReadResource};
10use crate::redacted_error::RedactedError;
11
12/// A validated request to read one exact Pack Archive object.
13#[derive(Clone, Debug)]
14pub struct PackArchiveReadRequest {
15    source: Location,
16    limits: ReadLimits,
17}
18
19impl PackArchiveReadRequest {
20    /// Validates an exact-object source and retains its read limits.
21    pub fn new(source: Location, limits: ReadLimits) -> Result<Self, PackArchiveReadRequestError> {
22        if let Err(role_error) = source.require_object() {
23            return Err(PackArchiveReadRequestError::InvalidSourceRole {
24                location: source,
25                source: role_error,
26            });
27        }
28
29        Ok(Self { source, limits })
30    }
31
32    /// The normalized exact-object source.
33    pub fn source(&self) -> &Location {
34        &self.source
35    }
36
37    /// The mandatory finite Pack Archive Read limits.
38    pub const fn limits(&self) -> ReadLimits {
39        self.limits
40    }
41}
42
43/// A reason a Pack Archive Read request is invalid.
44#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
45#[non_exhaustive]
46pub enum PackArchiveReadRequestError {
47    #[error("Pack Archive source {location} is not an exact object: {source}")]
48    InvalidSourceRole {
49        location: Location,
50        #[source]
51        source: LocationRoleError,
52    },
53}
54
55/// Reads exact Pack Archive bytes without decoding or validating them.
56///
57/// Decoding borrows the read bytes, so a decode failure leaves the exact
58/// bytes available for inspection or replay:
59///
60/// ```no_run
61/// use typst_pack::opendal::OperatorBindings;
62/// use typst_pack::opendal::pack_archive::{
63///     PackArchiveReadRequest, read_pack_archive,
64/// };
65/// use typst_pack::pack_archive::{DecodeLimits, decode};
66///
67/// async fn read_then_decode(
68///     bindings: &OperatorBindings,
69///     request: &PackArchiveReadRequest,
70/// ) -> Result<(), Box<dyn std::error::Error>> {
71///     let archive = read_pack_archive(bindings, request).await?;
72///     if let Err(decode_error) = decode(&archive, DecodeLimits::reference_v1()) {
73///         // Decoding borrowed `archive`; the exact read bytes are retained.
74///         let retry_bytes = archive.as_slice();
75///         eprintln!("decode failed for {} retained bytes: {decode_error}", retry_bytes.len());
76///     }
77///     Ok(())
78/// }
79/// ```
80#[allow(clippy::result_large_err)]
81pub async fn read_pack_archive<R: OperatorResolver + ?Sized>(
82    resolver: &R,
83    request: &PackArchiveReadRequest,
84) -> Result<PackArchiveBytes, PackArchiveReadError> {
85    let error = |cause| PackArchiveReadError {
86        source_location: request.source().clone(),
87        cause: RedactedError::new(cause),
88    };
89    let mut operators = ResolvedOperators::new(resolver);
90    let resolved = operators
91        .resolve(request.source().binding())
92        .map_err(|source| error(PackArchiveReadErrorCause::ResolveOperator(Box::new(source))))?;
93    if !resolved.read {
94        return Err(error(PackArchiveReadErrorCause::ReadUnsupported));
95    }
96    let ceiling = request.limits().archive_bytes();
97    let operation = PackArchiveExactPathOperation { request };
98    let bytes = read_exact_path(
99        &resolved.operator,
100        request.source().dispatch_path(),
101        ceiling,
102        ceiling,
103        &operation,
104    )
105    .await?
106    .ok_or_else(|| {
107        operation.error(PackArchiveReadErrorCause::ObjectAbsent(
108            exact_path_absent_error(),
109        ))
110    })?;
111
112    Ok(PackArchiveBytes::from_vec(bytes))
113}
114
115/// A failure while reading exact Pack Archive bytes through OpenDAL.
116///
117/// Rendering the complete source chain may disclose backend endpoints, bucket
118/// names, or other backend-provided context.
119#[derive(Debug, thiserror::Error)]
120#[error(
121    "Pack Archive Read failed for binding {binding} at exact-object operation path {operation_path:?}: {cause}",
122    binding = .source_location.binding(),
123    operation_path = .source_location.operation_path(),
124)]
125pub struct PackArchiveReadError {
126    source_location: Location,
127    #[source]
128    cause: RedactedError<PackArchiveReadErrorCause>,
129}
130
131impl PackArchiveReadError {
132    /// The normalized exact-object source that failed.
133    pub fn source_location(&self) -> &Location {
134        &self.source_location
135    }
136
137    /// The typed cause of the read failure.
138    pub fn cause(&self) -> &PackArchiveReadErrorCause {
139        self.cause.inner()
140    }
141}
142
143/// The typed cause of an OpenDAL Pack Archive Read failure.
144#[derive(Debug, thiserror::Error)]
145#[non_exhaustive]
146pub enum PackArchiveReadErrorCause {
147    #[error("operator resolution failed")]
148    ResolveOperator(#[source] BoxError),
149    #[error("read capability is unsupported")]
150    ReadUnsupported,
151    #[error("the exact object is absent")]
152    ObjectAbsent(#[source] ::opendal::Error),
153    #[error("the exact object read failed")]
154    Read(#[source] ::opendal::Error),
155    #[error("the archive byte limit failed")]
156    Limit(#[source] ReadLimitError),
157}
158
159struct PackArchiveExactPathOperation<'a> {
160    request: &'a PackArchiveReadRequest,
161}
162
163impl PackArchiveExactPathOperation<'_> {
164    fn error(&self, cause: PackArchiveReadErrorCause) -> PackArchiveReadError {
165        PackArchiveReadError {
166            source_location: self.request.source().clone(),
167            cause: RedactedError::new(cause),
168        }
169    }
170}
171
172impl ExactPathReadOperation for PackArchiveExactPathOperation<'_> {
173    type Error = PackArchiveReadError;
174
175    fn read(&self, source: ::opendal::Error) -> PackArchiveReadError {
176        self.error(PackArchiveReadErrorCause::Read(source))
177    }
178
179    fn limit_exceeded(&self, ceiling: u64, _: u64) -> PackArchiveReadError {
180        self.error(PackArchiveReadErrorCause::Limit(ReadLimitError::exceeded(
181            ReadResource::ArchiveBytes,
182            ceiling,
183        )))
184    }
185
186    fn accounting_overflow(&self) -> PackArchiveReadError {
187        self.error(PackArchiveReadErrorCause::Limit(
188            ReadLimitError::AccountingOverflow {
189                resource: ReadResource::ArchiveBytes,
190            },
191        ))
192    }
193}