blob: 6f1a5c2f5f5564cf931793a03b25e3d8aba6e017 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![cfg(target_os = "fuchsia")]
#![deny(missing_docs)]
//! Wrapper types for [`fidl_fuchsia_pkg::PackageCacheProxy`] and its related protocols.
use {
crate::types::{BlobId, BlobInfo},
fidl_fuchsia_pkg as fpkg,
fuchsia_pkg::PackageDirectory,
fuchsia_zircon_status::Status,
futures::prelude::*,
std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
mod storage;
/// An open connection to a provider of the `fuchsia.pkg.PackageCache`.
#[derive(Debug, Clone)]
pub struct Client {
proxy: fpkg::PackageCacheProxy,
}
impl Client {
/// Constructs a client from the given proxy.
pub fn from_proxy(proxy: fpkg::PackageCacheProxy) -> Self {
Self { proxy }
}
/// Returns a reference to the underlying PackageCacheProxy connection.
pub fn proxy(&self) -> &fpkg::PackageCacheProxy {
&self.proxy
}
/// Opens the package specified by `meta_far_blob` with the intent to fetch any missing blobs
/// using the returned [`Get`] type if needed.
pub fn get(
&self,
meta_far_blob: BlobInfo,
gc_protection: fpkg::GcProtection,
) -> Result<Get, fidl::Error> {
let (needed_blobs, needed_blobs_server_end) =
fidl::endpoints::create_proxy::<fpkg::NeededBlobsMarker>()?;
let (pkg_dir, pkg_dir_server_end) = PackageDirectory::create_request()?;
let get_fut = self.proxy.get(
&meta_far_blob.into(),
gc_protection,
needed_blobs_server_end,
pkg_dir_server_end,
);
Ok(Get {
get_fut,
pkg_dir,
needed_blobs,
pkg_present: SharedBoolEvent::new(),
meta_far: meta_far_blob.blob_id,
})
}
/// Uses PackageCache.Get to obtain the package directory of a package that is already cached
/// (all blobs are already in blobfs).
/// Errors if the package is not already cached.
/// Always uses open package tracking GC protection, because OTA (the only client of Retained
/// GC protection), should never need to get an already cached package.
///
/// Compared to `get_cached`:
/// * Activates `meta_far_blob` in the dynamic index
/// * Must not be called concurrently with the same `meta_far_blob`
pub async fn get_already_cached(
&self,
meta_far_blob: BlobId,
) -> Result<PackageDirectory, GetAlreadyCachedError> {
let mut get = self
.get(
BlobInfo { blob_id: meta_far_blob, length: 0 },
fpkg::GcProtection::OpenPackageTracking,
)
.map_err(GetAlreadyCachedError::Get)?;
if let Some(_) = get.open_meta_blob().await.map_err(GetAlreadyCachedError::OpenMetaBlob)? {
return Err(GetAlreadyCachedError::MissingMetaFar);
}
if let Some(missing_blobs) = get
.get_missing_blobs()
.try_next()
.await
.map_err(GetAlreadyCachedError::GetMissingBlobs)?
{
return Err(GetAlreadyCachedError::MissingContentBlobs(missing_blobs));
}
get.finish().await.map_err(GetAlreadyCachedError::FinishGet)
}
/// Uses PackageCache.GetSubpackage to obtain the package directory of a subpackage.
/// Errors if there is not an open connection to the superpackage.
pub async fn get_subpackage(
&self,
superpackage: BlobId,
subpackage: &fuchsia_url::RelativePackageUrl,
) -> Result<PackageDirectory, GetSubpackageError> {
let (dir, dir_server_end) =
PackageDirectory::create_request().map_err(GetSubpackageError::CreatingHandles)?;
let () = self
.proxy
.get_subpackage(
&superpackage.into(),
&fpkg::PackageUrl { url: subpackage.into() },
dir_server_end,
)
.await
.map_err(GetSubpackageError::CallingGetSubpackage)??;
Ok(dir)
}
}
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum GetAlreadyCachedError {
#[error("calling get")]
Get(#[source] fidl::Error),
#[error("opening meta blob")]
OpenMetaBlob(#[source] OpenBlobError),
#[error("meta.far blob not cached")]
MissingMetaFar,
#[error("getting missing blobs")]
GetMissingBlobs(#[source] ListMissingBlobsError),
#[error("content blobs not cached {0:?}")]
MissingContentBlobs(Vec<BlobInfo>),
#[error("finishing get")]
FinishGet(#[source] GetError),
}
impl GetAlreadyCachedError {
/// Returns true if the get failed because the package was not cached.
pub fn was_not_cached(&self) -> bool {
use GetAlreadyCachedError::*;
match self {
Get(..) | OpenMetaBlob(..) | GetMissingBlobs(..) | FinishGet(..) => false,
MissingMetaFar | MissingContentBlobs(..) => true,
}
}
}
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum GetSubpackageError {
#[error("creating handles")]
CreatingHandles(#[source] fidl::Error),
#[error("calling GetCached FIDL")]
CallingGetSubpackage(#[source] fidl::Error),
#[error("the superpackage does not have an open package connection")]
SuperpackageClosed,
#[error("the subpackage does not exist")]
DoesNotExist,
#[error("internal")]
Internal,
}
impl From<fpkg::GetSubpackageError> for GetSubpackageError {
fn from(fidl: fpkg::GetSubpackageError) -> Self {
use {fpkg::GetSubpackageError as fErr, GetSubpackageError::*};
match fidl {
fErr::SuperpackageClosed => SuperpackageClosed,
fErr::DoesNotExist => DoesNotExist,
fErr::Internal => Internal,
}
}
}
#[derive(Debug, Clone)]
struct SharedBoolEvent(Arc<AtomicBool>);
impl SharedBoolEvent {
fn new() -> Self {
Self(Arc::new(AtomicBool::new(false)))
}
fn get(&self) -> bool {
self.0.load(Ordering::SeqCst)
}
fn set(&self) {
self.0.store(true, Ordering::SeqCst)
}
}
async fn open_blob(
needed_blobs: &fpkg::NeededBlobsProxy,
kind: OpenKind,
blob_id: BlobId,
pkg_present: Option<&SharedBoolEvent>,
) -> Result<Option<NeededBlob>, OpenBlobError> {
let open_fut = match kind {
OpenKind::Meta => needed_blobs.open_meta_blob(),
OpenKind::Content => needed_blobs.open_blob(&blob_id.into()),
};
match open_fut.await {
Err(fidl::Error::ClientChannelClosed { status: Status::OK, .. }) => {
if let Some(pkg_present) = pkg_present {
pkg_present.set();
}
Ok(None)
}
res => {
if let Some(blob) = res?? {
let (writer, closer) = self::storage::into_blob_writer_and_closer(*blob)?;
Ok(Some(NeededBlob {
blob: Blob {
writer,
needed_blobs: needed_blobs.clone(),
blob_id,
state: NeedsTruncate,
},
closer: BlobCloser { closer, closed: false },
}))
} else {
Ok(None)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpenKind {
Meta,
Content,
}
/// A deferred call to [`Get::open_meta_blob`] or [`Get::open_blob`].
#[derive(Debug)]
pub struct DeferredOpenBlob {
needed_blobs: fpkg::NeededBlobsProxy,
kind: OpenKind,
blob_id: BlobId,
pkg_present: Option<SharedBoolEvent>,
}
impl DeferredOpenBlob {
/// Opens the blob for write, if it is still needed. The blob's data can be provided using the
/// returned NeededBlob.
pub async fn open(&self) -> Result<Option<NeededBlob>, OpenBlobError> {
open_blob(&self.needed_blobs, self.kind, self.blob_id, self.pkg_present.as_ref()).await
}
fn proxy_cmp_key(&self) -> u32 {
use fidl::{endpoints::Proxy, AsHandleRef};
self.needed_blobs.as_channel().raw_handle()
}
}
impl std::cmp::PartialEq for DeferredOpenBlob {
fn eq(&self, other: &Self) -> bool {
self.proxy_cmp_key() == other.proxy_cmp_key() && self.kind == other.kind
}
}
impl std::cmp::Eq for DeferredOpenBlob {}
/// A pending `fuchsia.pkg/PackageCache.Get()` request. Clients must, in order:
/// 1. open/write the meta blob, if Some(NeededBlob) is provided by that API
/// 2. enumerate all missing content blobs
/// 3. open/write all missing content blobs, if Some(NeededBlob) is provided by that API
/// 4. finish() to complete the Get() request.
#[derive(Debug)]
pub struct Get {
get_fut: fidl::client::QueryResponseFut<Result<(), i32>>,
needed_blobs: fpkg::NeededBlobsProxy,
pkg_dir: PackageDirectory,
pkg_present: SharedBoolEvent,
meta_far: BlobId,
}
impl Get {
/// Returns an independent object that can be used to open the meta blob for write. See
/// [`Self::open_meta_blob`].
pub fn make_open_meta_blob(&mut self) -> DeferredOpenBlob {
DeferredOpenBlob {
needed_blobs: self.needed_blobs.clone(),
kind: OpenKind::Meta,
blob_id: self.meta_far,
pkg_present: Some(self.pkg_present.clone()),
}
}
/// Opens the meta blob for write, if it is still needed. The blob's data can be provided using
/// the returned NeededBlob.
pub async fn open_meta_blob(&mut self) -> Result<Option<NeededBlob>, OpenBlobError> {
open_blob(&self.needed_blobs, OpenKind::Meta, self.meta_far, Some(&self.pkg_present)).await
}
fn start_get_missing_blobs(
&mut self,
) -> Result<Option<fpkg::BlobInfoIteratorProxy>, fidl::Error> {
if self.pkg_present.get() {
return Ok(None);
}
let (blob_iterator, blob_iterator_server_end) =
fidl::endpoints::create_proxy::<fpkg::BlobInfoIteratorMarker>()?;
self.needed_blobs.get_missing_blobs(blob_iterator_server_end)?;
Ok(Some(blob_iterator))
}
/// Determines the set of blobs that the caller must open/write to complete this `Get()`
/// operation.
/// The returned stream will never yield an empty `Vec`.
/// Callers should process the missing blobs (via `make_open_blob` or `open_blob`) concurrently
/// with reading the stream to guarantee stream termination.
pub fn get_missing_blobs(
&mut self,
) -> impl Stream<Item = Result<Vec<BlobInfo>, ListMissingBlobsError>> + Unpin {
match self.start_get_missing_blobs() {
Ok(option_iter) => match option_iter {
Some(iterator) => crate::fidl_iterator_to_stream(iterator)
.map_ok(|v| v.into_iter().map(BlobInfo::from).collect())
.map_err(ListMissingBlobsError::CallNextOnBlobIterator)
.left_stream(),
None => futures::stream::empty().right_stream(),
}
.left_stream(),
Err(e) => {
futures::stream::iter(Some(Err(ListMissingBlobsError::CallGetMissingBlobs(e))))
.right_stream()
}
}
}
/// Returns an independent object that can be used to open the `content_blob` for write. See
/// [`Self::open_blob`].
pub fn make_open_blob(&mut self, content_blob: BlobId) -> DeferredOpenBlob {
DeferredOpenBlob {
needed_blobs: self.needed_blobs.clone(),
kind: OpenKind::Content,
blob_id: content_blob,
pkg_present: None,
}
}
/// Opens `content_blob` for write, if it is still needed. The blob's data can be provided
/// using the returned NeededBlob.
pub async fn open_blob(
&mut self,
content_blob: BlobId,
) -> Result<Option<NeededBlob>, OpenBlobError> {
open_blob(&self.needed_blobs, OpenKind::Content, content_blob, None).await
}
/// Notifies the endpoint that all blobs have been written and wait for the response to the
/// pending `Get()` request, returning the cached [`PackageDirectory`].
pub async fn finish(self) -> Result<PackageDirectory, GetError> {
drop(self.needed_blobs);
let () = self.get_fut.await?.map_err(Status::from_raw)?;
Ok(self.pkg_dir)
}
/// Aborts this caching operation for the package.
pub async fn abort(self) {
self.needed_blobs.abort().map(|_: Result<(), fidl::Error>| ()).await;
// The package is not guaranteed to be removed from the dynamic index after abort
// returns, we have to wait until finish returns (to prevent a resolve retry from
// racing). The finish call will return an error that just tells us that we called
// abort, so we ignore it.
let _ = self.get_fut.await;
}
}
/// A blob that needs to be written.
#[derive(Debug)]
pub struct NeededBlob {
/// Typestate wrapper around the blob. Clients must first call truncate(), then write() until
/// all data is provided.
pub blob: Blob<NeedsTruncate>,
/// Helper object that can close the blob independent of what state `blob` is in.
pub closer: BlobCloser,
}
/// A handle to a blob that must be explicitly closed to prevent future opens of the same blob from
/// racing with this blob closing.
#[derive(Debug)]
#[must_use = "Subsequent opens of this blob may race with closing this one"]
pub struct BlobCloser {
closer: Box<dyn self::storage::Closer>,
closed: bool,
}
impl BlobCloser {
/// Close the blob, silently ignoring errors.
pub async fn close(mut self) {
let () = self.closer.close().await;
self.closed = true;
}
}
impl Drop for BlobCloser {
fn drop(&mut self) {
if !self.closed {
let () = self.closer.best_effort_close();
}
}
}
/// The successful result of truncating a blob.
#[derive(Debug)]
pub enum TruncateBlobSuccess {
/// The blob contents need to be written.
NeedsData(Blob<NeedsData>),
/// The blob is fully written (it was the empty blob) and now a
/// fuchsia.pkg.NeededBlobs.BlobWritten message should be sent.
AllWritten(Blob<NeedsBlobWritten>),
}
/// The successful result of writing some data to a blob.
#[derive(Debug)]
pub enum BlobWriteSuccess {
/// There is still more data to write.
NeedsData(Blob<NeedsData>),
/// The blob is fully written and now a fuchsia.pkg.NeededBlobs.BlobWritten
/// message should be sent.
AllWritten(Blob<NeedsBlobWritten>),
}
/// State for a blob that can be truncated.
#[derive(Debug)]
pub struct NeedsTruncate;
/// State for a blob that can be written to.
#[derive(Debug)]
pub struct NeedsData {
size: u64,
written: u64,
}
/// State for a blob that has been fully written but that needs a
/// fuchsia.pkg.NeededBlobs.BlobWritten message sent to pkg-cache.
#[derive(Debug)]
pub struct NeedsBlobWritten;
/// A blob in the process of being written.
#[derive(Debug)]
#[must_use]
pub struct Blob<S> {
writer: Box<dyn self::storage::Writer>,
needed_blobs: fpkg::NeededBlobsProxy,
blob_id: BlobId,
state: S,
}
impl Blob<NeedsTruncate> {
/// Truncates the blob to the given size. On success, the blob enters the writable state.
pub async fn truncate(mut self, size: u64) -> Result<TruncateBlobSuccess, TruncateBlobError> {
let () = self.writer.truncate(size).await?;
let Self { writer, needed_blobs, blob_id, state: _ } = self;
Ok(if size == 0 {
TruncateBlobSuccess::AllWritten(Blob {
writer,
needed_blobs,
blob_id,
state: NeedsBlobWritten,
})
} else {
TruncateBlobSuccess::NeedsData(Blob {
writer,
needed_blobs,
blob_id,
state: NeedsData { size, written: 0 },
})
})
}
}
impl Blob<NeedsData> {
/// Writes all of the given buffer to the blob.
///
/// # Panics
///
/// Panics if a write is attempted with a buf larger than the remaining blob size.
pub fn write(
self,
buf: &[u8],
) -> impl Future<Output = Result<BlobWriteSuccess, WriteBlobError>> + '_ {
self.write_with_trace_callbacks(buf, &|_| {}, &|| {})
}
/// Writes all of the given buffer to the blob.
///
/// `after_write` and `after_write_ack` are called before and after, respectively, waiting for
/// the server to acknowledge writes.
/// They may be called multiple times if the write of `buf` is chunked.
/// `after_write` is given the size of each write in bytes.
/// Useful for creating trace spans.
///
/// # Panics
///
/// Panics if a write is attempted with a buf larger than the remaining blob size.
pub async fn write_with_trace_callbacks(
mut self,
buf: &[u8],
after_write: &(dyn Fn(u64) + Send + Sync),
after_write_ack: &(dyn Fn() + Send + Sync),
) -> Result<BlobWriteSuccess, WriteBlobError> {
assert!(self.state.written + buf.len() as u64 <= self.state.size);
let () = self.writer.write(buf, after_write, after_write_ack).await?;
self.state.written += buf.len() as u64;
if self.state.written == self.state.size {
let Self { writer, needed_blobs, blob_id, state: _ } = self;
Ok(BlobWriteSuccess::AllWritten(Blob {
writer,
needed_blobs,
blob_id,
state: NeedsBlobWritten,
}))
} else {
Ok(BlobWriteSuccess::NeedsData(self))
}
}
}
impl Blob<NeedsBlobWritten> {
/// Tells pkg-cache that the blob has been successfully written and can now be read.
pub async fn blob_written(self) -> Result<(), BlobWrittenError> {
Ok(self.needed_blobs.blob_written(&self.blob_id.into()).await??)
}
}
/// An error encountered while opening a package.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum OpenError {
#[error("the package does not exist")]
NotFound,
#[error("Open() responded with an unexpected status")]
UnexpectedResponse(#[source] Status),
#[error("transport error")]
Fidl(#[from] fidl::Error),
}
/// An error encountered while caching a package.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum GetError {
#[error("Get() responded with an unexpected status")]
UnexpectedResponse(#[from] Status),
#[error("transport error")]
Fidl(#[from] fidl::Error),
}
/// An error encountered while opening a metadata or content blob for write.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum OpenBlobError {
#[error("there is insufficient storage space available to persist this blob")]
OutOfSpace,
#[error("this blob is already open for write by another cache operation")]
ConcurrentWrite,
#[error("an unspecified error occurred during underlying I/O")]
UnspecifiedIo,
#[error("an unspecified error occurred")]
Internal,
#[error("transport error")]
Fidl(#[from] fidl::Error),
}
impl From<fpkg::OpenBlobError> for OpenBlobError {
fn from(e: fpkg::OpenBlobError) -> Self {
match e {
fpkg::OpenBlobError::OutOfSpace => OpenBlobError::OutOfSpace,
fpkg::OpenBlobError::ConcurrentWrite => OpenBlobError::ConcurrentWrite,
fpkg::OpenBlobError::UnspecifiedIo => OpenBlobError::UnspecifiedIo,
fpkg::OpenBlobError::Internal => OpenBlobError::Internal,
}
}
}
/// An error encountered while enumerating missing content blobs.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum ListMissingBlobsError {
#[error("while obtaining the missing blobs fidl iterator")]
CallGetMissingBlobs(#[source] fidl::Error),
#[error("while obtaining the next chunk of blobs from the fidl iterator")]
CallNextOnBlobIterator(#[source] fidl::Error),
}
/// An error encountered while truncating a blob
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum TruncateBlobError {
#[error("insufficient storage space is available")]
NoSpace,
#[error("Truncate() responded with an unexpected status")]
UnexpectedResponse(#[source] Status),
#[error("transport error")]
Fidl(#[from] fidl::Error),
#[error("already truncated, currently in state {0}")]
AlreadyTruncated(&'static str),
// TODO(https://fxbug.dev/42080352) Add error variants to BlobWriter.
#[error("unspecified error")]
Other(#[source] anyhow::Error),
#[error("blob is in an invalid state")]
BadState,
}
/// An error encountered while writing a blob.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum WriteBlobError {
#[error("file endpoint reported it wrote more bytes than were actually provided to the file endpoint")]
Overwrite,
#[error("the written data was corrupt")]
Corrupt,
#[error("insufficient storage space is available")]
NoSpace,
#[error("Write() responded with an unexpected status")]
UnexpectedResponse(#[source] Status),
#[error("transport error")]
Fidl(#[from] fidl::Error),
#[error("bytes were written but not needed in state {0}")]
BytesNotNeeded(&'static str),
// TODO(https://fxbug.dev/42080352) Add error variants to BlobWriter.
#[error("unspecified error")]
Other(#[source] anyhow::Error),
}
/// An error encountered while sending the BlobWritten message.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum BlobWrittenError {
#[error("pkg-cache could not find the blob after it was successfully written")]
MissingAfterWritten,
#[error("NeededBlobs.BlobWritten was called before the blob was opened")]
UnopenedBlob,
#[error("transport error")]
Fidl(#[from] fidl::Error),
}
impl From<fpkg::BlobWrittenError> for BlobWrittenError {
fn from(e: fpkg::BlobWrittenError) -> Self {
match e {
fpkg::BlobWrittenError::NotWritten => BlobWrittenError::MissingAfterWritten,
fpkg::BlobWrittenError::UnopenedBlob => BlobWrittenError::UnopenedBlob,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use fidl::endpoints::{ClientEnd, ControlHandle as _, RequestStream as _};
use fidl_fuchsia_io as fio;
use fidl_fuchsia_pkg::{
BlobInfoIteratorRequest, NeededBlobsRequest, NeededBlobsRequestStream,
PackageCacheGetResponder, PackageCacheMarker, PackageCacheRequest,
PackageCacheRequestStream,
};
struct MockPackageCache {
stream: PackageCacheRequestStream,
}
impl MockPackageCache {
fn new() -> (Client, Self) {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<PackageCacheMarker>().unwrap();
(Client::from_proxy(proxy), Self { stream })
}
async fn expect_get(
&mut self,
blob_info: BlobInfo,
expected_gc_protection: fpkg::GcProtection,
) -> PendingGet {
match self.stream.next().await {
Some(Ok(PackageCacheRequest::Get {
meta_far_blob,
gc_protection,
needed_blobs,
dir,
responder,
})) => {
assert_eq!(BlobInfo::from(meta_far_blob), blob_info);
assert_eq!(gc_protection, expected_gc_protection);
let needed_blobs = needed_blobs.into_stream().unwrap();
let dir = dir.into_stream().unwrap();
PendingGet { stream: needed_blobs, dir, responder }
}
r => panic!("Unexpected request: {:?}", r),
}
}
async fn expect_closed(mut self) {
assert_matches!(self.stream.next().await, None);
}
}
struct PendingGet {
stream: NeededBlobsRequestStream,
dir: fio::DirectoryRequestStream,
responder: PackageCacheGetResponder,
}
impl PendingGet {
async fn new() -> (Get, PendingGet) {
let (client, mut server) = MockPackageCache::new();
let get = client.get(blob_info(42), fpkg::GcProtection::OpenPackageTracking).unwrap();
let pending_get =
server.expect_get(blob_info(42), fpkg::GcProtection::OpenPackageTracking).await;
(get, pending_get)
}
fn finish_hold_stream_open(self) -> (NeededBlobsRequestStream, PackageDirProvider) {
self.stream.control_handle().shutdown_with_epitaph(Status::OK);
self.responder.send(Ok(())).unwrap();
(self.stream, PackageDirProvider { stream: self.dir })
}
fn finish(self) -> PackageDirProvider {
self.stream.control_handle().shutdown_with_epitaph(Status::OK);
self.responder.send(Ok(())).unwrap();
PackageDirProvider { stream: self.dir }
}
#[cfg(target_os = "fuchsia")]
fn fail_the_get(self) {
self.responder
.send(Err(Status::IO_INVALID.into_raw()))
.expect("client should be waiting");
}
async fn expect_open_meta_blob(
mut self,
res: Result<Option<ClientEnd<fio::FileMarker>>, fpkg::OpenBlobError>,
) -> Self {
match self.stream.next().await {
Some(Ok(NeededBlobsRequest::OpenMetaBlob { responder })) => {
responder.send(res.map(|o| o.map(fpkg::BlobWriter::File))).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_open_blob(
mut self,
expected_blob_id: BlobId,
res: Result<Option<ClientEnd<fio::FileMarker>>, fpkg::OpenBlobError>,
) -> Self {
match self.stream.next().await {
Some(Ok(NeededBlobsRequest::OpenBlob { blob_id, responder })) => {
assert_eq!(BlobId::from(blob_id), expected_blob_id);
responder.send(res.map(|o| o.map(fpkg::BlobWriter::File))).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_get_missing_blobs(mut self, response_chunks: Vec<Vec<BlobInfo>>) -> Self {
match self.stream.next().await {
Some(Ok(NeededBlobsRequest::GetMissingBlobs { iterator, control_handle: _ })) => {
let mut stream = iterator.into_stream().unwrap();
// Respond to each next request with the next chunk.
for chunk in response_chunks {
let chunk = chunk
.into_iter()
.map(fidl_fuchsia_pkg::BlobInfo::from)
.collect::<Vec<_>>();
let BlobInfoIteratorRequest::Next { responder } =
stream.next().await.unwrap().unwrap();
responder.send(&chunk).unwrap();
}
// Then respond with an empty chunk.
let BlobInfoIteratorRequest::Next { responder } =
stream.next().await.unwrap().unwrap();
responder.send(&[]).unwrap();
// Expect the client to stop asking.
assert_matches!(stream.next().await, None);
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_get_missing_blobs_client_closes_channel(
mut self,
response_chunks: Vec<Vec<BlobInfo>>,
) -> Self {
match self.stream.next().await {
Some(Ok(NeededBlobsRequest::GetMissingBlobs { iterator, control_handle: _ })) => {
let mut stream = iterator.into_stream().unwrap();
// Respond to each next request with the next chunk.
for chunk in response_chunks {
let chunk = chunk
.into_iter()
.map(fidl_fuchsia_pkg::BlobInfo::from)
.collect::<Vec<_>>();
let BlobInfoIteratorRequest::Next { responder } =
stream.next().await.unwrap().unwrap();
responder.send(&chunk).unwrap();
}
// The client closes the channel before we can respond with an empty chunk.
assert_matches!(stream.next().await, None);
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_get_missing_blobs_inject_iterator_error(mut self) -> Self {
match self.stream.next().await {
Some(Ok(NeededBlobsRequest::GetMissingBlobs { iterator, control_handle: _ })) => {
iterator
.into_stream_and_control_handle()
.unwrap()
.1
.shutdown_with_epitaph(Status::ADDRESS_IN_USE);
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
#[cfg(target_os = "fuchsia")]
async fn expect_abort(mut self) -> Self {
match self.stream.next().await {
Some(Ok(NeededBlobsRequest::Abort { responder })) => {
responder.send().unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
}
struct PackageDirProvider {
stream: fio::DirectoryRequestStream,
}
impl PackageDirProvider {
fn close_pkg_dir(self) {
self.stream.control_handle().shutdown_with_epitaph(Status::NOT_EMPTY);
}
}
fn blob_id(n: u8) -> BlobId {
BlobId::from([n; 32])
}
fn blob_info(n: u8) -> BlobInfo {
BlobInfo { blob_id: blob_id(n), length: 0 }
}
#[fuchsia_async::run_singlethreaded(test)]
async fn constructor() {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<PackageCacheMarker>().unwrap();
let client = Client::from_proxy(proxy);
drop(stream);
assert_matches!(client.proxy().sync().await, Err(_));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn get_present_package() {
let (client, mut server) = MockPackageCache::new();
let ((), ()) = future::join(
async {
server
.expect_get(blob_info(2), fpkg::GcProtection::OpenPackageTracking)
.await
.finish()
.close_pkg_dir();
server.expect_closed().await;
},
async move {
let mut get =
client.get(blob_info(2), fpkg::GcProtection::OpenPackageTracking).unwrap();
assert_matches!(get.open_meta_blob().await.unwrap(), None);
assert_eq!(get.get_missing_blobs().try_concat().await.unwrap(), vec![]);
let pkg_dir = get.finish().await.unwrap();
assert_matches!(
pkg_dir.into_proxy().take_event_stream().next().await,
Some(Err(fidl::Error::ClientChannelClosed { status: Status::NOT_EMPTY, .. }))
);
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn get_present_package_handles_slow_stream_close() {
let (client, mut server) = MockPackageCache::new();
let (send, recv) = futures::channel::oneshot::channel::<()>();
let ((), ()) = future::join(
async {
let (needed_blobs_stream, pkg_dir) = server
.expect_get(blob_info(2), fpkg::GcProtection::OpenPackageTracking)
.await
.finish_hold_stream_open();
pkg_dir.close_pkg_dir();
// wait until `send` is dropped to drop the request stream.
let _ = recv.await;
drop(needed_blobs_stream);
},
async move {
let mut get =
client.get(blob_info(2), fpkg::GcProtection::OpenPackageTracking).unwrap();
assert_matches!(get.open_meta_blob().await.unwrap(), None);
// ensure sending the request doesn't fail, then unblock closing the channel, then
// ensure the get_missing_blobs call detects the closed iterator as success instead
// of a PEER_CLOSED error.
let missing_blobs_stream = get.get_missing_blobs();
drop(send);
assert_eq!(missing_blobs_stream.try_concat().await.unwrap(), vec![]);
let pkg_dir = get.finish().await.unwrap();
assert_matches!(
pkg_dir.into_proxy().take_event_stream().next().await,
Some(Err(fidl::Error::ClientChannelClosed { status: Status::NOT_EMPTY, .. }))
);
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn needed_blobs_open_meta_far() {
let (mut get, pending_get) = PendingGet::new().await;
let ((), ()) = future::join(
async {
pending_get
.expect_open_meta_blob(Ok(None))
.await
.expect_open_meta_blob(Ok(Some(fidl::endpoints::create_endpoints().0)))
.await
.expect_open_meta_blob(Ok(None))
.await
.expect_open_meta_blob(Ok(Some(fidl::endpoints::create_endpoints().0)))
.await
.expect_open_meta_blob(Err(fpkg::OpenBlobError::OutOfSpace))
.await
.expect_open_meta_blob(Err(fpkg::OpenBlobError::UnspecifiedIo))
.await;
},
async {
{
let opener = get.make_open_meta_blob();
assert_matches!(opener.open().await.unwrap(), None);
assert_matches!(opener.open().await.unwrap(), Some(_));
}
assert_matches!(get.open_meta_blob().await.unwrap(), None);
assert_matches!(get.open_meta_blob().await.unwrap(), Some(_));
assert_matches!(get.open_meta_blob().await, Err(OpenBlobError::OutOfSpace));
assert_matches!(get.open_meta_blob().await, Err(OpenBlobError::UnspecifiedIo));
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn needed_blobs_open_content_blob() {
let (mut get, pending_get) = PendingGet::new().await;
let ((), ()) = future::join(
async {
pending_get
.expect_open_blob(blob_id(2), Ok(None))
.await
.expect_open_blob(blob_id(2), Ok(Some(fidl::endpoints::create_endpoints().0)))
.await
.expect_open_blob(blob_id(10), Ok(None))
.await
.expect_open_blob(blob_id(11), Ok(Some(fidl::endpoints::create_endpoints().0)))
.await
.expect_open_blob(blob_id(12), Err(fpkg::OpenBlobError::OutOfSpace))
.await
.expect_open_blob(blob_id(13), Err(fpkg::OpenBlobError::UnspecifiedIo))
.await;
},
async {
{
let opener = get.make_open_blob(blob_id(2));
assert_matches!(opener.open().await.unwrap(), None);
assert_matches!(opener.open().await.unwrap(), Some(_));
}
assert_matches!(get.open_blob(blob_id(10),).await.unwrap(), None);
assert_matches!(get.open_blob(blob_id(11),).await.unwrap(), Some(_));
assert_matches!(get.open_blob(blob_id(12),).await, Err(OpenBlobError::OutOfSpace));
assert_matches!(
get.open_blob(blob_id(13),).await,
Err(OpenBlobError::UnspecifiedIo)
);
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn needed_blobs_get_missing_blobs_on_closed_ok() {
let (mut get, pending_get) = PendingGet::new().await;
let _ = pending_get.finish();
assert_matches!(get.open_meta_blob().await, Ok(None));
assert_eq!(get.get_missing_blobs().try_concat().await.unwrap(), vec![]);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn needed_blobs_get_missing_blobs() {
let (mut get, pending_get) = PendingGet::new().await;
let ((), ()) = future::join(
async {
pending_get
.expect_get_missing_blobs(vec![
vec![blob_info(1), blob_info(2)],
vec![blob_info(3)],
])
.await;
},
async {
assert_eq!(
get.get_missing_blobs().try_concat().await.unwrap(),
vec![blob_info(1), blob_info(2), blob_info(3)]
);
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn needed_blobs_get_missing_blobs_fail_to_obtain_iterator() {
let (mut get, pending_get) = PendingGet::new().await;
drop(pending_get);
assert_matches!(
get.get_missing_blobs().try_concat().await,
Err(ListMissingBlobsError::CallNextOnBlobIterator(
fidl::Error::ClientChannelClosed{status, ..})
)
if status == Status::PEER_CLOSED
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn needed_blobs_get_missing_blobs_iterator_contains_error() {
let (mut get, pending_get) = PendingGet::new().await;
let (_, ()) =
future::join(pending_get.expect_get_missing_blobs_inject_iterator_error(), async {
assert_matches!(
get.get_missing_blobs().try_concat().await,
Err(ListMissingBlobsError::CallNextOnBlobIterator(
fidl::Error::ClientChannelClosed{status, ..}
))
if status == Status::ADDRESS_IN_USE
);
})
.await;
}
#[cfg(target_os = "fuchsia")]
#[test]
fn needed_blobs_abort() {
use futures::{future::Either, pin_mut};
use std::task::Poll;
let mut executor = fuchsia_async::TestExecutor::new_with_fake_time();
let fut = async {
let (get, pending_get) = PendingGet::new().await;
let abort_fut = get.abort().boxed();
let expect_abort_fut = pending_get.expect_abort();
pin_mut!(expect_abort_fut);
match futures::future::select(abort_fut, expect_abort_fut).await {
Either::Left(((), _expect_abort_fut)) => {
panic!("abort should wait for the get future to complete")
}
Either::Right((pending_get, abort_fut)) => (abort_fut, pending_get),
}
};
pin_mut!(fut);
let (mut abort_fut, pending_get) = match executor.run_until_stalled(&mut fut) {
Poll::Pending => panic!("should complete"),
Poll::Ready((abort_fut, pending_get)) => (abort_fut, pending_get),
};
// NeededBlobs.Abort should wait until PackageCache.Get returns
assert_matches!(executor.run_until_stalled(&mut abort_fut), Poll::Pending);
pending_get.fail_the_get();
assert_matches!(executor.run_until_stalled(&mut abort_fut), Poll::Ready(()));
}
struct MockNeededBlob {
blob: fio::FileRequestStream,
needed_blobs: fpkg::NeededBlobsRequestStream,
}
impl MockNeededBlob {
fn mock_hash() -> BlobId {
[7; 32].into()
}
fn new() -> (NeededBlob, Self) {
let (blob_proxy, blob) =
fidl::endpoints::create_proxy_and_stream::<fio::FileMarker>().unwrap();
let (needed_blobs_proxy, needed_blobs) =
fidl::endpoints::create_proxy_and_stream::<fpkg::NeededBlobsMarker>().unwrap();
(
NeededBlob {
blob: Blob {
writer: Box::new(Clone::clone(&blob_proxy)),
needed_blobs: needed_blobs_proxy,
blob_id: Self::mock_hash(),
state: NeedsTruncate,
},
closer: BlobCloser { closer: Box::new(blob_proxy), closed: false },
},
Self { blob, needed_blobs },
)
}
async fn fail_truncate(mut self) -> Self {
match self.blob.next().await {
Some(Ok(fio::FileRequest::Resize { length: _, responder })) => {
responder.send(Err(Status::NO_SPACE.into_raw())).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_truncate(mut self, expected_length: u64) -> Self {
match self.blob.next().await {
Some(Ok(fio::FileRequest::Resize { length, responder })) => {
assert_eq!(length, expected_length);
responder.send(Ok(())).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn fail_write(mut self) -> Self {
match self.blob.next().await {
Some(Ok(fio::FileRequest::Write { data: _, responder })) => {
responder.send(Err(Status::NO_SPACE.into_raw())).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_write(mut self, expected_payload: &[u8]) -> Self {
match self.blob.next().await {
Some(Ok(fio::FileRequest::Write { data, responder })) => {
assert_eq!(data, expected_payload);
responder.send(Ok(data.len() as u64)).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_write_partial(
mut self,
expected_payload: &[u8],
bytes_to_consume: u64,
) -> Self {
match self.blob.next().await {
Some(Ok(fio::FileRequest::Write { data, responder })) => {
assert_eq!(data, expected_payload);
responder.send(Ok(bytes_to_consume)).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
async fn expect_close(mut self) {
match self.blob.next().await {
Some(Ok(fio::FileRequest::Close { responder })) => {
responder.send(Ok(())).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
}
async fn expect_blob_written(mut self) -> Self {
match self.needed_blobs.next().await {
Some(Ok(fpkg::NeededBlobsRequest::BlobWritten { blob_id, responder })) => {
assert_eq!(blob_id, Self::mock_hash().into());
responder.send(Ok(())).unwrap();
}
r => panic!("Unexpected request: {:?}", r),
}
self
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn empty_blob_write() {
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server
.expect_truncate(0)
.await
.expect_blob_written()
.await
.expect_close()
.await;
},
async {
let blob = match blob.truncate(0).await.unwrap() {
TruncateBlobSuccess::AllWritten(blob) => blob,
other => panic!("empty blob shouldn't need bytes {other:?}"),
};
let () = blob.blob_written().await.unwrap();
closer.close().await;
},
)
.await;
}
impl TruncateBlobSuccess {
fn unwrap_needs_data(self) -> Blob<NeedsData> {
match self {
TruncateBlobSuccess::NeedsData(blob) => blob,
TruncateBlobSuccess::AllWritten(_) => panic!("blob should need data"),
}
}
}
impl BlobWriteSuccess {
fn unwrap_needs_data(self) -> Blob<NeedsData> {
match self {
BlobWriteSuccess::NeedsData(blob) => blob,
BlobWriteSuccess::AllWritten(_) => panic!("blob should need data"),
}
}
fn unwrap_all_written(self) -> Blob<NeedsBlobWritten> {
match self {
BlobWriteSuccess::NeedsData(_) => panic!("blob should be completely written"),
BlobWriteSuccess::AllWritten(blob) => blob,
}
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn small_blob_write() {
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server
.expect_truncate(4)
.await
.expect_write(b"test")
.await
.expect_blob_written()
.await
.expect_close()
.await;
},
async {
let blob = blob.truncate(4).await.unwrap().unwrap_needs_data();
let blob = blob.write(b"test").await.unwrap().unwrap_all_written();
let () = blob.blob_written().await.unwrap();
closer.close().await;
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blob_truncate_no_space() {
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server.fail_truncate().await;
},
async {
assert_matches!(blob.truncate(4).await, Err(TruncateBlobError::NoSpace));
closer.close().await;
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blob_write_no_space() {
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server.expect_truncate(4).await.fail_write().await;
},
async {
let blob = blob.truncate(4).await.unwrap().unwrap_needs_data();
assert_matches!(blob.write(b"test").await, Err(WriteBlobError::NoSpace));
closer.close().await;
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blob_write_server_partial_write() {
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server
.expect_truncate(6)
.await
.expect_write_partial(b"abc123", 3)
.await
.expect_write(b"123")
.await
.expect_blob_written()
.await;
},
async {
let blob = blob.truncate(6).await.unwrap().unwrap_needs_data();
let blob = blob.write(b"abc123").await.unwrap().unwrap_all_written();
let () = blob.blob_written().await.unwrap();
closer.close().await;
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blob_write_client_partial_write() {
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server
.expect_truncate(6)
.await
.expect_write(b"abc")
.await
.expect_write(b"123")
.await
.expect_blob_written()
.await;
},
async {
let blob = blob.truncate(6).await.unwrap().unwrap_needs_data();
let blob = blob.write(b"abc").await.unwrap().unwrap_needs_data();
let blob = blob.write(b"123").await.unwrap().unwrap_all_written();
let () = blob.blob_written().await.unwrap();
closer.close().await;
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blob_write_chunkize_payload() {
const CHUNK_SIZE: usize = fio::MAX_BUF as usize;
let (NeededBlob { blob, closer }, blob_server) = MockNeededBlob::new();
let ((), ()) = future::join(
async {
blob_server
.expect_truncate(3 * CHUNK_SIZE as u64)
.await
.expect_write(&[0u8; CHUNK_SIZE])
.await
.expect_write(&[1u8; CHUNK_SIZE])
.await
.expect_write(&[2u8; CHUNK_SIZE])
.await
.expect_blob_written()
.await;
},
async {
let payload =
(0..3).flat_map(|n| std::iter::repeat(n).take(CHUNK_SIZE)).collect::<Vec<u8>>();
let blob = blob.truncate(3 * CHUNK_SIZE as u64).await.unwrap().unwrap_needs_data();
let blob = blob.write(&payload).await.unwrap().unwrap_all_written();
let () = blob.blob_written().await.unwrap();
closer.close().await;
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn get_already_cached_success() {
let (client, mut server) = MockPackageCache::new();
let ((), ()) = future::join(
async {
server
.expect_get(blob_info(2), fpkg::GcProtection::OpenPackageTracking)
.await
.finish()
.close_pkg_dir();
server.expect_closed().await;
},
async move {
let pkg_dir = client.get_already_cached(blob_id(2)).await.unwrap();
assert_matches!(
pkg_dir.into_proxy().take_event_stream().next().await,
Some(Err(fidl::Error::ClientChannelClosed { status: Status::NOT_EMPTY, .. }))
);
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn get_already_cached_missing_meta_far() {
let (client, mut server) = MockPackageCache::new();
let ((), ()) = future::join(
async {
server
.expect_get(blob_info(2), fpkg::GcProtection::OpenPackageTracking)
.await
.expect_open_meta_blob(Ok(Some(fidl::endpoints::create_endpoints().0)))
.await;
},
async move {
assert_matches!(
client.get_already_cached(blob_id(2)).await,
Err(GetAlreadyCachedError::MissingMetaFar)
);
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn get_already_cached_missing_content_blob() {
let (client, mut server) = MockPackageCache::new();
let ((), ()) = future::join(
async {
server
.expect_get(blob_info(2), fpkg::GcProtection::OpenPackageTracking)
.await
.expect_open_meta_blob(Ok(None))
.await
.expect_get_missing_blobs_client_closes_channel(vec![vec![BlobInfo {
blob_id: [0; 32].into(),
length: 0,
}]])
.await;
},
async move {
assert_matches!(
client.get_already_cached(blob_id(2)).await,
Err(GetAlreadyCachedError::MissingContentBlobs(v))
if v == vec![BlobInfo {
blob_id: [0; 32].into(),
length: 0,
}]
);
},
)
.await;
}
}