rename appmgr

This commit is contained in:
Aiden McClelland
2022-01-21 19:02:23 -07:00
committed by Aiden McClelland
parent 9cf379f9ee
commit edde478382
124 changed files with 25 additions and 45 deletions

130
backend/src/s9pk/builder.rs Normal file
View File

@@ -0,0 +1,130 @@
use std::io::{Read, Seek, SeekFrom, Write};
use digest::Digest;
use sha2::Sha512;
use tracing::instrument;
use typed_builder::TypedBuilder;
use super::header::{FileSection, Header};
use super::manifest::Manifest;
use super::SIG_CONTEXT;
use crate::util::HashWriter;
use crate::{Error, ResultExt};
#[derive(TypedBuilder)]
pub struct S9pkPacker<
'a,
W: Write + Seek,
RLicense: Read,
RInstructions: Read,
RIcon: Read,
RDockerImages: Read,
RAssets: Read,
> {
writer: W,
manifest: &'a Manifest,
license: RLicense,
instructions: RInstructions,
icon: RIcon,
docker_images: RDockerImages,
assets: RAssets,
}
impl<
'a,
W: Write + Seek,
RLicense: Read,
RInstructions: Read,
RIcon: Read,
RDockerImages: Read,
RAssets: Read,
> S9pkPacker<'a, W, RLicense, RInstructions, RIcon, RDockerImages, RAssets>
{
/// BLOCKING
#[instrument(skip(self))]
pub fn pack(mut self, key: &ed25519_dalek::Keypair) -> Result<(), Error> {
let header_pos = self.writer.stream_position()?;
if header_pos != 0 {
tracing::warn!("Appending to non-empty file.");
}
let mut header = Header::placeholder();
header.serialize(&mut self.writer).with_ctx(|_| {
(
crate::ErrorKind::Serialization,
"Writing Placeholder Header",
)
})?;
let mut position = self.writer.stream_position()?;
let mut writer = HashWriter::new(Sha512::new(), &mut self.writer);
// manifest
serde_cbor::ser::into_writer(self.manifest, &mut writer).with_ctx(|_| {
(
crate::ErrorKind::Serialization,
"Serializing Manifest (CBOR)",
)
})?;
let new_pos = writer.inner_mut().stream_position()?;
header.table_of_contents.manifest = FileSection {
position,
length: new_pos - position,
};
position = new_pos;
// license
std::io::copy(&mut self.license, &mut writer)
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying License"))?;
let new_pos = writer.inner_mut().stream_position()?;
header.table_of_contents.license = FileSection {
position,
length: new_pos - position,
};
position = new_pos;
// instructions
std::io::copy(&mut self.instructions, &mut writer)
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Instructions"))?;
let new_pos = writer.inner_mut().stream_position()?;
header.table_of_contents.instructions = FileSection {
position,
length: new_pos - position,
};
position = new_pos;
// icon
std::io::copy(&mut self.icon, &mut writer)
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Icon"))?;
let new_pos = writer.inner_mut().stream_position()?;
header.table_of_contents.icon = FileSection {
position,
length: new_pos - position,
};
position = new_pos;
// docker_images
std::io::copy(&mut self.docker_images, &mut writer)
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Docker Images"))?;
let new_pos = writer.inner_mut().stream_position()?;
header.table_of_contents.docker_images = FileSection {
position,
length: new_pos - position,
};
position = new_pos;
// assets
std::io::copy(&mut self.assets, &mut writer)
.with_ctx(|_| (crate::ErrorKind::Filesystem, "Copying Assets"))?;
let new_pos = writer.inner_mut().stream_position()?;
header.table_of_contents.assets = FileSection {
position,
length: new_pos - position,
};
position = new_pos;
// header
let (hash, _) = writer.finish();
self.writer.seek(SeekFrom::Start(header_pos))?;
header.pubkey = key.public.clone();
header.signature = key.sign_prehashed(hash, Some(SIG_CONTEXT))?;
header
.serialize(&mut self.writer)
.with_ctx(|_| (crate::ErrorKind::Serialization, "Writing Header"))?;
self.writer.seek(SeekFrom::Start(position))?;
Ok(())
}
}

173
backend/src/s9pk/header.rs Normal file
View File

@@ -0,0 +1,173 @@
use std::collections::BTreeMap;
use std::io::Write;
use color_eyre::eyre::eyre;
use ed25519_dalek::{PublicKey, Signature};
use tokio::io::{AsyncRead, AsyncReadExt};
use crate::Error;
pub const MAGIC: [u8; 2] = [59, 59];
pub const VERSION: u8 = 1;
#[derive(Debug)]
pub struct Header {
pub pubkey: PublicKey,
pub signature: Signature,
pub table_of_contents: TableOfContents,
}
impl Header {
pub fn placeholder() -> Self {
Header {
pubkey: PublicKey::default(),
signature: Signature::new([0; 64]),
table_of_contents: Default::default(),
}
}
// MUST BE SAME SIZE REGARDLESS OF DATA
pub fn serialize<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
writer.write_all(&MAGIC)?;
writer.write_all(&[VERSION])?;
writer.write_all(self.pubkey.as_bytes())?;
writer.write_all(self.signature.as_ref())?;
self.table_of_contents.serialize(writer)?;
Ok(())
}
pub async fn deserialize<R: AsyncRead + Unpin>(mut reader: R) -> Result<Self, Error> {
let mut magic = [0; 2];
reader.read_exact(&mut magic).await?;
if magic != MAGIC {
return Err(Error::new(
eyre!("Incorrect Magic"),
crate::ErrorKind::ParseS9pk,
));
}
let mut version = [0];
reader.read_exact(&mut version).await?;
if version[0] != VERSION {
return Err(Error::new(
eyre!("Unknown Version"),
crate::ErrorKind::ParseS9pk,
));
}
let mut pubkey_bytes = [0; 32];
reader.read_exact(&mut pubkey_bytes).await?;
let pubkey = PublicKey::from_bytes(&pubkey_bytes)
.map_err(|e| Error::new(e, crate::ErrorKind::ParseS9pk))?;
let mut sig_bytes = [0; 64];
reader.read_exact(&mut sig_bytes).await?;
let signature = Signature::new(sig_bytes);
let table_of_contents = TableOfContents::deserialize(reader).await?;
Ok(Header {
pubkey,
signature,
table_of_contents,
})
}
}
#[derive(Debug, Default)]
pub struct TableOfContents {
pub manifest: FileSection,
pub license: FileSection,
pub instructions: FileSection,
pub icon: FileSection,
pub docker_images: FileSection,
pub assets: FileSection,
}
impl TableOfContents {
pub fn serialize<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
let len: u32 = ((1 + "manifest".len() + 16)
+ (1 + "license".len() + 16)
+ (1 + "instructions".len() + 16)
+ (1 + "icon".len() + 16)
+ (1 + "docker_images".len() + 16)
+ (1 + "assets".len() + 16)) as u32;
writer.write_all(&u32::to_be_bytes(len))?;
self.manifest.serialize_entry("manifest", &mut writer)?;
self.license.serialize_entry("license", &mut writer)?;
self.instructions
.serialize_entry("instructions", &mut writer)?;
self.icon.serialize_entry("icon", &mut writer)?;
self.docker_images
.serialize_entry("docker_images", &mut writer)?;
self.assets.serialize_entry("assets", &mut writer)?;
Ok(())
}
pub async fn deserialize<R: AsyncRead + Unpin>(mut reader: R) -> std::io::Result<Self> {
let mut toc_len = [0; 4];
reader.read_exact(&mut toc_len).await?;
let toc_len = u32::from_be_bytes(toc_len);
let mut reader = reader.take(toc_len as u64);
let mut table = BTreeMap::new();
while let Some((label, section)) = FileSection::deserialize_entry(&mut reader).await? {
table.insert(label, section);
}
fn from_table(
table: &BTreeMap<Vec<u8>, FileSection>,
label: &str,
) -> std::io::Result<FileSection> {
table.get(label.as_bytes()).copied().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("Missing Required Label: {}", label),
)
})
}
#[allow(dead_code)]
fn as_opt(fs: FileSection) -> Option<FileSection> {
if fs.position | fs.length == 0 {
// 0/0 is not a valid file section
None
} else {
Some(fs)
}
}
Ok(TableOfContents {
manifest: from_table(&table, "manifest")?,
license: from_table(&table, "license")?,
instructions: from_table(&table, "instructions")?,
icon: from_table(&table, "icon")?,
docker_images: from_table(&table, "docker_images")?,
assets: from_table(&table, "assets")?,
})
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FileSection {
pub position: u64,
pub length: u64,
}
impl FileSection {
pub fn serialize_entry<W: Write>(self, label: &str, mut writer: W) -> std::io::Result<()> {
writer.write_all(&[label.len() as u8])?;
writer.write_all(label.as_bytes())?;
writer.write_all(&u64::to_be_bytes(self.position))?;
writer.write_all(&u64::to_be_bytes(self.length))?;
Ok(())
}
pub async fn deserialize_entry<R: AsyncRead + Unpin>(
mut reader: R,
) -> std::io::Result<Option<(Vec<u8>, Self)>> {
let mut label_len = [0];
let read = reader.read(&mut label_len).await?;
if read == 0 {
return Ok(None);
}
let mut label = vec![0; label_len[0] as usize];
reader.read_exact(&mut label).await?;
let mut pos = [0; 8];
reader.read_exact(&mut pos).await?;
let mut len = [0; 8];
reader.read_exact(&mut len).await?;
Ok(Some((
label,
FileSection {
position: u64::from_be_bytes(pos),
length: u64::from_be_bytes(len),
},
)))
}
}

View File

@@ -0,0 +1,222 @@
use std::borrow::Borrow;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use patch_db::HasModel;
use serde::{Deserialize, Serialize, Serializer};
use url::Url;
use crate::action::{ActionImplementation, Actions};
use crate::backup::BackupActions;
use crate::config::action::ConfigActions;
use crate::dependencies::Dependencies;
use crate::id::{Id, InvalidId, SYSTEM_ID};
use crate::migration::Migrations;
use crate::net::interface::Interfaces;
use crate::status::health_check::HealthChecks;
use crate::util::Version;
use crate::version::{Current, VersionT};
use crate::volume::Volumes;
pub const SYSTEM_PACKAGE_ID: PackageId<&'static str> = PackageId(SYSTEM_ID);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PackageId<S: AsRef<str> = String>(Id<S>);
impl<'a> PackageId<&'a str> {
pub fn owned(&self) -> PackageId {
PackageId(self.0.owned())
}
}
impl FromStr for PackageId {
type Err = InvalidId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(PackageId(Id::try_from(s.to_owned())?))
}
}
impl From<PackageId> for String {
fn from(value: PackageId) -> Self {
value.0.into()
}
}
impl<S: AsRef<str>> From<Id<S>> for PackageId<S> {
fn from(id: Id<S>) -> Self {
PackageId(id)
}
}
impl<S: AsRef<str>> std::ops::Deref for PackageId<S> {
type Target = S;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl<S: AsRef<str>> AsRef<PackageId<S>> for PackageId<S> {
fn as_ref(&self) -> &PackageId<S> {
self
}
}
impl<S: AsRef<str>> std::fmt::Display for PackageId<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
impl<S: AsRef<str>> AsRef<str> for PackageId<S> {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl<S: AsRef<str>> Borrow<str> for PackageId<S> {
fn borrow(&self) -> &str {
self.0.as_ref()
}
}
impl<S: AsRef<str>> AsRef<Path> for PackageId<S> {
fn as_ref(&self) -> &Path {
self.0.as_ref().as_ref()
}
}
impl<'de, S> Deserialize<'de> for PackageId<S>
where
S: AsRef<str>,
Id<S>: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
Ok(PackageId(Deserialize::deserialize(deserializer)?))
}
}
impl<S> Serialize for PackageId<S>
where
S: AsRef<str>,
{
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
where
Ser: Serializer,
{
Serialize::serialize(&self.0, serializer)
}
}
fn current_version() -> Version {
Current::new().semver().into()
}
#[derive(Clone, Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
pub struct Manifest {
#[serde(default = "current_version")]
pub eos_version: Version,
pub id: PackageId,
pub title: String,
#[model]
pub version: Version,
pub description: Description,
#[serde(default)]
pub assets: Assets,
#[serde(default)]
pub build: Option<Vec<String>>,
pub release_notes: String,
pub license: String, // type of license
pub wrapper_repo: Url,
pub upstream_repo: Url,
pub support_site: Option<Url>,
pub marketing_site: Option<Url>,
pub donation_url: Option<Url>,
#[serde(default)]
pub alerts: Alerts,
#[model]
pub main: ActionImplementation,
pub health_checks: HealthChecks,
#[model]
pub config: Option<ConfigActions>,
#[model]
pub properties: Option<ActionImplementation>,
#[model]
pub volumes: Volumes,
// #[serde(default)]
pub interfaces: Interfaces,
// #[serde(default)]
#[model]
pub backup: BackupActions,
#[serde(default)]
#[model]
pub migrations: Migrations,
#[serde(default)]
pub actions: Actions,
// #[serde(default)]
// pub permissions: Permissions,
#[serde(default)]
#[model]
pub dependencies: Dependencies,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Assets {
#[serde(default)]
pub license: Option<PathBuf>,
#[serde(default)]
pub instructions: Option<PathBuf>,
#[serde(default)]
pub icon: Option<PathBuf>,
#[serde(default)]
pub docker_images: Option<PathBuf>,
#[serde(default)]
pub assets: Option<PathBuf>,
}
impl Assets {
pub fn license_path(&self) -> &Path {
self.license
.as_ref()
.map(|a| a.as_path())
.unwrap_or(Path::new("LICENSE.md"))
}
pub fn instructions_path(&self) -> &Path {
self.instructions
.as_ref()
.map(|a| a.as_path())
.unwrap_or(Path::new("INSTRUCTIONS.md"))
}
pub fn icon_path(&self) -> &Path {
self.icon
.as_ref()
.map(|a| a.as_path())
.unwrap_or(Path::new("icon.png"))
}
pub fn icon_type(&self) -> &str {
self.icon
.as_ref()
.and_then(|icon| icon.extension())
.and_then(|ext| ext.to_str())
.unwrap_or("png")
}
pub fn docker_images_path(&self) -> &Path {
self.docker_images
.as_ref()
.map(|a| a.as_path())
.unwrap_or(Path::new("image.tar"))
}
pub fn assets_path(&self) -> &Path {
self.assets
.as_ref()
.map(|a| a.as_path())
.unwrap_or(Path::new("assets"))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Description {
pub short: String,
pub long: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Alerts {
pub install: Option<String>,
pub uninstall: Option<String>,
pub restore: Option<String>,
pub start: Option<String>,
pub stop: Option<String>,
}

116
backend/src/s9pk/mod.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::path::PathBuf;
use color_eyre::eyre::eyre;
use rpc_toolkit::command;
use tracing::instrument;
use crate::context::SdkContext;
use crate::s9pk::builder::S9pkPacker;
use crate::s9pk::manifest::Manifest;
use crate::s9pk::reader::S9pkReader;
use crate::util::display_none;
use crate::volume::Volume;
use crate::{Error, ResultExt};
pub mod builder;
pub mod header;
pub mod manifest;
pub mod reader;
pub const SIG_CONTEXT: &'static [u8] = b"s9pk";
#[command(cli_only, display(display_none), blocking)]
#[instrument(skip(ctx))]
pub fn pack(#[context] ctx: SdkContext, #[arg] path: Option<PathBuf>) -> Result<(), Error> {
use std::fs::File;
use std::io::Read;
let path = if let Some(path) = path {
path
} else {
std::env::current_dir()?
};
let manifest: Manifest = if path.join("manifest.toml").exists() {
let mut s = String::new();
File::open(path.join("manifest.toml"))?.read_to_string(&mut s)?;
serde_toml::from_str(&s).with_kind(crate::ErrorKind::Deserialization)?
} else if path.join("manifest.yaml").exists() {
serde_yaml::from_reader(File::open(path.join("manifest.yaml"))?)
.with_kind(crate::ErrorKind::Deserialization)?
} else if path.join("manifest.json").exists() {
serde_json::from_reader(File::open(path.join("manifest.json"))?)
.with_kind(crate::ErrorKind::Deserialization)?
} else {
return Err(Error::new(
eyre!("manifest not found"),
crate::ErrorKind::Pack,
));
};
let outfile_path = path.join(format!("{}.s9pk", manifest.id));
let mut outfile = File::create(outfile_path)?;
S9pkPacker::builder()
.manifest(&manifest)
.writer(&mut outfile)
.license(
File::open(path.join(manifest.assets.license_path())).with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
manifest.assets.license_path().display().to_string(),
)
})?,
)
.icon(
File::open(path.join(manifest.assets.icon_path())).with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
manifest.assets.icon_path().display().to_string(),
)
})?,
)
.instructions(
File::open(path.join(manifest.assets.instructions_path())).with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
manifest.assets.instructions_path().display().to_string(),
)
})?,
)
.docker_images(
File::open(path.join(manifest.assets.docker_images_path())).with_ctx(|_| {
(
crate::ErrorKind::Filesystem,
manifest.assets.docker_images_path().display().to_string(),
)
})?,
)
.assets({
let mut assets = tar::Builder::new(Vec::new()); // TODO: Ideally stream this? best not to buffer in memory
for (asset_volume, _) in manifest
.volumes
.iter()
.filter(|(_, v)| matches!(v, &&Volume::Assets {}))
{
assets.append_dir_all(
asset_volume,
path.join(manifest.assets.assets_path()).join(asset_volume),
)?;
}
std::io::Cursor::new(assets.into_inner()?)
})
.build()
.pack(&ctx.developer_key()?)?;
outfile.sync_all()?;
Ok(())
}
#[command(cli_only, display(display_none))]
pub async fn verify(#[arg] path: PathBuf) -> Result<(), Error> {
let mut s9pk = S9pkReader::open(path, true).await?;
s9pk.validate().await?;
Ok(())
}

170
backend/src/s9pk/reader.rs Normal file
View File

@@ -0,0 +1,170 @@
use std::io::SeekFrom;
use std::path::Path;
use std::pin::Pin;
use std::task::{Context, Poll};
use digest::Output;
use sha2::{Digest, Sha512};
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, ReadBuf, Take};
use tracing::instrument;
use super::header::{FileSection, Header, TableOfContents};
use super::manifest::Manifest;
use super::SIG_CONTEXT;
use crate::install::progress::InstallProgressTracker;
use crate::{Error, ResultExt};
#[pin_project::pin_project]
pub struct ReadHandle<'a, R: AsyncRead + AsyncSeek + Unpin = File> {
pos: &'a mut u64,
#[pin]
rdr: Take<&'a mut R>,
}
impl<'a, R: AsyncRead + AsyncSeek + Unpin> ReadHandle<'a, R> {
pub async fn to_vec(mut self) -> std::io::Result<Vec<u8>> {
let mut buf = vec![0; self.rdr.limit() as usize];
self.rdr.read_exact(&mut buf).await?;
Ok(buf)
}
}
impl<'a, R: AsyncRead + AsyncSeek + Unpin> AsyncRead for ReadHandle<'a, R> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let start = buf.filled().len();
let this = self.project();
let pos = this.pos;
AsyncRead::poll_read(this.rdr, cx, buf).map(|res| {
**pos += (buf.filled().len() - start) as u64;
res
})
}
}
pub struct S9pkReader<R: AsyncRead + AsyncSeek + Unpin = File> {
hash: Option<Output<Sha512>>,
hash_string: Option<String>,
toc: TableOfContents,
pos: u64,
rdr: R,
}
impl S9pkReader {
pub async fn open<P: AsRef<Path>>(path: P, check_sig: bool) -> Result<Self, Error> {
let p = path.as_ref();
let rdr = File::open(p)
.await
.with_ctx(|_| (crate::error::ErrorKind::Filesystem, p.display().to_string()))?;
Self::from_reader(rdr, check_sig).await
}
}
impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<InstallProgressTracker<R>> {
pub fn validated(&mut self) {
self.rdr.validated()
}
}
impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<R> {
#[instrument(skip(self))]
pub async fn validate(&mut self) -> Result<(), Error> {
Ok(())
}
#[instrument(skip(rdr))]
pub async fn from_reader(mut rdr: R, check_sig: bool) -> Result<Self, Error> {
let header = Header::deserialize(&mut rdr).await?;
let (hash, hash_string) = if check_sig {
let mut hasher = Sha512::new();
let mut buf = [0; 1024];
let mut read;
while {
read = rdr.read(&mut buf).await?;
read != 0
} {
hasher.update(&buf[0..read]);
}
let hash = hasher.clone().finalize();
header
.pubkey
.verify_prehashed(hasher, Some(SIG_CONTEXT), &header.signature)?;
(
Some(hash),
Some(base32::encode(
base32::Alphabet::RFC4648 { padding: false },
hash.as_slice(),
)),
)
} else {
(None, None)
};
let pos = rdr.stream_position().await?;
Ok(S9pkReader {
hash_string,
hash,
toc: header.table_of_contents,
pos,
rdr,
})
}
pub fn hash(&self) -> Option<&Output<Sha512>> {
self.hash.as_ref()
}
pub fn hash_str(&self) -> Option<&str> {
self.hash_string.as_ref().map(|s| s.as_str())
}
pub async fn reset(&mut self) -> Result<(), Error> {
self.rdr.seek(SeekFrom::Start(0)).await?;
Ok(())
}
async fn read_handle<'a>(
&'a mut self,
section: FileSection,
) -> Result<ReadHandle<'a, R>, Error> {
if self.pos != section.position {
self.rdr.seek(SeekFrom::Start(section.position)).await?;
self.pos = section.position;
}
Ok(ReadHandle {
pos: &mut self.pos,
rdr: (&mut self.rdr).take(section.length),
})
}
pub async fn manifest_raw<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
self.read_handle(self.toc.manifest).await
}
pub async fn manifest(&mut self) -> Result<Manifest, Error> {
let slice = self.manifest_raw().await?.to_vec().await?;
serde_cbor::de::from_reader(slice.as_slice())
.with_ctx(|_| (crate::ErrorKind::ParseS9pk, "Deserializing Manifest (CBOR)"))
}
pub async fn license<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
Ok(self.read_handle(self.toc.license).await?)
}
pub async fn instructions<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
Ok(self.read_handle(self.toc.instructions).await?)
}
pub async fn icon<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
Ok(self.read_handle(self.toc.icon).await?)
}
pub async fn docker_images<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
Ok(self.read_handle(self.toc.docker_images).await?)
}
pub async fn assets<'a>(&'a mut self) -> Result<ReadHandle<'a, R>, Error> {
Ok(self.read_handle(self.toc.assets).await?)
}
}