Merge branch 'feature/scoreboard_management' into develop

This commit is contained in:
Moritz Hölting 2025-04-07 14:07:32 +02:00
commit ed62e75e89
16 changed files with 209 additions and 49 deletions

View File

@ -3,7 +3,7 @@ on:
push: push:
branches: branches:
- main - main
- development - develop
- 'releases/**' - 'releases/**'
pull_request: pull_request:

View File

@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- support for commands using macros - support for commands using macros
- support for registering scoreboards (automatic creation and deletion)
### Changed ### Changed
- use "return" command for conditionals instead of data storage when using supported pack format - use "return" command for conditionals instead of data storage when using supported pack format
- update latest datapack format to 61
### Removed ### Removed

View File

@ -20,12 +20,16 @@ serde = ["dep:serde"]
zip = ["dep:zip"] zip = ["dep:zip"]
[dependencies] [dependencies]
chksum-md5 = "0.0.0" chksum-md5 = "0.1.0"
getset = "0.1.2" getset = "0.1.5"
serde = { version = "1.0.197", optional = true, features = ["derive"] } serde = { version = "1.0.219", optional = true, features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.140"
tracing = "0.1.40" tracing = "0.1.41"
zip = { version = "2.1.3", default-features = false, features = ["deflate", "time"], optional = true } zip = { version = "2.6.1", default-features = false, features = ["deflate", "time"], optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3.13.0" tempfile = "3.19.1"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View File

@ -18,7 +18,7 @@ use shulkerbox::{
util::compile::CompileOptions, util::compile::CompileOptions,
}; };
let mut dp = Datapack::new("shulkerpack", 20) // Create a new datapack with the name "shulkerpack" and the pack format 20 let mut dp = Datapack::new("shulker", 20) // Create a new datapack with the name "shulkerpack" and the pack format 20
.with_description("I created this datapack with rust") // Add a description to the datapack .with_description("I created this datapack with rust") // Add a description to the datapack
.with_supported_formats(16..=20) // Add the supported formats of the datapack .with_supported_formats(16..=20) // Add the supported formats of the datapack
.with_template_folder(Path::new("./template")) // Add a template folder to the datapack. This will include all files in the template folder in the root of the datapack and can be used for including the "pack.png" file .with_template_folder(Path::new("./template")) // Add a template folder to the datapack. This will include all files in the template folder in the root of the datapack and can be used for including the "pack.png" file

View File

@ -5,10 +5,12 @@ use shulkerbox::prelude::*;
fn main() { fn main() {
// create a new datapack // create a new datapack
let mut dp = Datapack::new(16).with_supported_formats(16..=20); let mut dp = Datapack::new("example", 16).with_supported_formats(16..=20);
// get the namespace "test" dp.register_scoreboard("example_scoreboard", None::<&str>, None::<&str>);
let namespace = dp.namespace_mut("test");
// get the namespace "example"
let namespace = dp.namespace_mut("example");
// get the function "foo" of the namespace "test" and add some commands // get the function "foo" of the namespace "test" and add some commands
let foo_function = namespace.function_mut("foo"); let foo_function = namespace.function_mut("foo");
@ -32,7 +34,7 @@ fn main() {
)), )),
))); )));
dp.add_load("test:foo"); dp.add_load("example:foo");
// compile the datapack // compile the datapack
let v_folder = dp.compile(&CompileOptions::default()); let v_folder = dp.compile(&CompileOptions::default());

View File

@ -0,0 +1,31 @@
use std::path::Path;
// import the prelude to get all the necessary structs
use shulkerbox::prelude::*;
fn main() {
let mut dp = Datapack::new("main", 20).with_supported_formats(16..=20);
// get the namespace "test"
let namespace = dp.namespace_mut("test");
// get the function "foo" of the namespace "test" and add some commands
let foo_function = namespace.function_mut("foo");
let ex = Execute::If(
Condition::from("entity A"),
Box::new(Execute::Run(Box::new("say A".into()))),
Some(Box::new(Execute::If(
Condition::from("entity B"),
Box::new(Execute::Run(Box::new("say B".into()))),
Some(Box::new(Execute::Run(Box::new("say C".into())))),
))),
);
foo_function.add_command(ex);
// compile the datapack
let v_folder = dp.compile(&CompileOptions::default());
// place the compiled datapack in the "./dist" folder
v_folder.place(Path::new("./dist")).unwrap();
}

View File

@ -439,7 +439,7 @@ impl Condition {
#[must_use] #[must_use]
pub fn to_truth_table(&self) -> Vec<Self> { pub fn to_truth_table(&self) -> Vec<Self> {
match self.normalize() { match self.normalize() {
Self::Atom(_) | Self::Not(_) => vec![self.clone()], Self::Atom(_) | Self::Not(_) => vec![self.normalize()],
Self::Or(a, b) => a Self::Or(a, b) => a
.to_truth_table() .to_truth_table()
.into_iter() .into_iter()
@ -461,7 +461,7 @@ impl Condition {
/// Convert the condition into a [`MacroString`]. /// Convert the condition into a [`MacroString`].
/// ///
/// Will fail if the condition contains an `Or` variant. Use `compile` instead. /// Will fail if the condition contains an `Or` or double nested `Not` variant. Use `compile` instead.
fn str_cond(&self) -> Option<MacroString> { fn str_cond(&self) -> Option<MacroString> {
match self { match self {
Self::Atom(s) => Some(MacroString::from("if ") + s.clone()), Self::Atom(s) => Some(MacroString::from("if ") + s.clone()),

View File

@ -234,7 +234,7 @@ impl Execute {
Self::If(_, next, el) => { Self::If(_, next, el) => {
pack_formats.start() >= &4 pack_formats.start() >= &4
&& next.validate(pack_formats) && next.validate(pack_formats)
&& el.as_deref().map_or(true, |el| el.validate(pack_formats)) && el.as_deref().is_none_or(|el| el.validate(pack_formats))
} }
Self::Summon(_, next) | Self::On(_, next) => { Self::Summon(_, next) | Self::On(_, next) => {
pack_formats.start() >= &12 && next.validate(pack_formats) pack_formats.start() >= &12 && next.validate(pack_formats)
@ -261,7 +261,7 @@ impl Execute {
Self::If(cond, then, el) => { Self::If(cond, then, el) => {
cond.contains_macro() cond.contains_macro()
|| then.contains_macro() || then.contains_macro()
|| el.as_deref().map_or(false, Self::contains_macro) || el.as_deref().is_some_and(Self::contains_macro)
} }
Self::Run(cmd) => cmd.contains_macro(), Self::Run(cmd) => cmd.contains_macro(),
Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macro), Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macro),

View File

@ -363,8 +363,8 @@ fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive<u8>) -> bool {
map map
}); });
cmd.split_ascii_whitespace().next().map_or(true, |cmd| { cmd.split_ascii_whitespace().next().is_none_or(|cmd| {
cmd_formats.get(cmd).map_or(true, |range| { cmd_formats.get(cmd).is_none_or(|range| {
let start_cmd = range.start(); let start_cmd = range.start();
let end_cmd = range.end(); let end_cmd = range.end();

View File

@ -25,11 +25,11 @@ pub struct Function {
} }
impl Function { impl Function {
pub(in crate::datapack) fn new(namespace: &str, name: &str) -> Self { pub(in crate::datapack) fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
Self { Self {
commands: Vec::new(), commands: Vec::new(),
name: name.to_string(), name: name.into(),
namespace: namespace.to_string(), namespace: namespace.into(),
} }
} }
/// Add a command to the function. /// Add a command to the function.

View File

@ -8,7 +8,7 @@ pub use command::{Command, Condition, Execute};
pub use function::Function; pub use function::Function;
pub use namespace::Namespace; pub use namespace::Namespace;
use std::{collections::HashMap, ops::RangeInclusive, sync::Mutex}; use std::{borrow::Cow, collections::BTreeMap, ops::RangeInclusive, sync::Mutex};
use crate::{ use crate::{
util::compile::{CompileOptions, CompilerState, MutCompilerState}, util::compile::{CompileOptions, CompilerState, MutCompilerState},
@ -23,21 +23,28 @@ pub struct Datapack {
description: String, description: String,
pack_format: u8, pack_format: u8,
supported_formats: Option<RangeInclusive<u8>>, supported_formats: Option<RangeInclusive<u8>>,
namespaces: HashMap<String, Namespace>, main_namespace_name: String,
namespaces: BTreeMap<String, Namespace>,
/// Scoreboard name -> (criteria, display name)
scoreboards: BTreeMap<String, (Option<String>, Option<String>)>,
uninstall_commands: Vec<Command>,
custom_files: VFolder, custom_files: VFolder,
} }
impl Datapack { impl Datapack {
pub const LATEST_FORMAT: u8 = 48; pub const LATEST_FORMAT: u8 = 61;
/// Create a new Minecraft datapack. /// Create a new Minecraft datapack.
#[must_use] #[must_use]
pub fn new(pack_format: u8) -> Self { pub fn new(main_namespace_name: impl Into<String>, pack_format: u8) -> Self {
Self { Self {
description: String::from("A Minecraft datapack created with shulkerbox"), description: String::from("A Minecraft datapack created with shulkerbox"),
pack_format, pack_format,
supported_formats: None, supported_formats: None,
namespaces: HashMap::new(), main_namespace_name: main_namespace_name.into(),
namespaces: BTreeMap::new(),
scoreboards: BTreeMap::new(),
uninstall_commands: Vec::new(),
custom_files: VFolder::new(), custom_files: VFolder::new(),
} }
} }
@ -65,6 +72,7 @@ impl Datapack {
/// # Errors /// # Errors
/// - If loading the directory fails /// - If loading the directory fails
#[cfg(feature = "fs_access")] #[cfg(feature = "fs_access")]
#[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))]
pub fn with_template_folder<P>(self, path: P) -> std::io::Result<Self> pub fn with_template_folder<P>(self, path: P) -> std::io::Result<Self>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
@ -90,24 +98,49 @@ impl Datapack {
} }
/// Mutably get a namespace by name or create a new one if it doesn't exist. /// Mutably get a namespace by name or create a new one if it doesn't exist.
pub fn namespace_mut(&mut self, name: &str) -> &mut Namespace { pub fn namespace_mut(&mut self, name: impl Into<String>) -> &mut Namespace {
let name = name.into();
self.namespaces self.namespaces
.entry(name.to_string()) .entry(name.clone())
.or_insert_with(|| Namespace::new(name)) .or_insert_with(|| Namespace::new(name))
} }
/// Add a function to the tick function list. /// Add a function to the tick function list.
pub fn add_tick(&mut self, function: &str) { pub fn add_tick(&mut self, function: impl Into<String>) {
self.namespace_mut("minecraft") self.namespace_mut("minecraft")
.tag_mut("tick", tag::TagType::Function) .tag_mut("tick", tag::TagType::Function)
.add_value(tag::TagValue::Simple(function.to_string())); .add_value(tag::TagValue::Simple(function.into()));
} }
/// Add a function to the load function list. /// Add a function to the load function list.
pub fn add_load(&mut self, function: &str) { pub fn add_load(&mut self, function: impl Into<String>) {
self.namespace_mut("minecraft") self.namespace_mut("minecraft")
.tag_mut("load", tag::TagType::Function) .tag_mut("load", tag::TagType::Function)
.add_value(tag::TagValue::Simple(function.to_string())); .add_value(tag::TagValue::Simple(function.into()));
}
/// Register a scoreboard.
pub fn register_scoreboard(
&mut self,
name: impl Into<String>,
criteria: Option<impl Into<String>>,
display_name: Option<impl Into<String>>,
) {
self.scoreboards.insert(
name.into(),
(criteria.map(Into::into), display_name.map(Into::into)),
);
}
/// Scoreboards registered in the datapack.
#[must_use]
pub fn scoreboards(&self) -> &BTreeMap<String, (Option<String>, Option<String>)> {
&self.scoreboards
}
/// Add commands to the uninstall function.
pub fn add_uninstall_commands(&mut self, commands: Vec<Command>) {
self.uninstall_commands.extend(commands);
} }
/// Add a custom file to the datapack. /// Add a custom file to the datapack.
@ -132,8 +165,71 @@ impl Datapack {
root_folder.add_file("pack.mcmeta", mcmeta); root_folder.add_file("pack.mcmeta", mcmeta);
let mut data_folder = VFolder::new(); let mut data_folder = VFolder::new();
let mut modified_namespaces = self
.namespaces
.iter()
.map(|(name, namespace)| (name.as_str(), Cow::Borrowed(namespace)))
.collect::<BTreeMap<_, _>>();
let mut uninstall_commands = options
.uninstall_function
.then_some(Cow::Borrowed(&self.uninstall_commands));
if !self.scoreboards.is_empty() {
let main_namespace = modified_namespaces
.entry(&self.main_namespace_name)
.or_insert_with(|| Cow::Owned(Namespace::new(&self.main_namespace_name)));
let register_scoreboard_function = main_namespace
.to_mut()
.function_mut("sb/register_scoreboards");
for (name, (criteria, display_name)) in &self.scoreboards {
let mut creation_command = format!(
"scoreboard objectives add {name} {criteria}",
criteria = criteria.as_deref().unwrap_or("dummy")
);
if let Some(display_name) = display_name {
creation_command.push(' ');
creation_command.push_str(display_name);
}
register_scoreboard_function.add_command(Command::Raw(creation_command));
if let Some(uninstall_commands) = uninstall_commands.as_mut() {
uninstall_commands
.to_mut()
.push(Command::Raw(format!("scoreboard objectives remove {name}")));
}
}
let minecraft_namespace = modified_namespaces
.entry("minecraft")
.or_insert_with(|| Cow::Owned(Namespace::new("minecraft")));
minecraft_namespace
.to_mut()
.tag_mut("load", tag::TagType::Function)
.values_mut()
.insert(
0,
tag::TagValue::Simple(format!(
"{}:sb/register_scoreboards",
self.main_namespace_name
)),
);
}
if let Some(uninstall_commands) = uninstall_commands {
if !uninstall_commands.is_empty() {
let main_namespace = modified_namespaces
.entry(&self.main_namespace_name)
.or_insert_with(|| Cow::Owned(Namespace::new(&self.main_namespace_name)));
let uninstall_function = main_namespace.to_mut().function_mut("uninstall");
uninstall_function
.get_commands_mut()
.extend(uninstall_commands.into_owned());
}
}
// Compile namespaces // Compile namespaces
for (name, namespace) in &self.namespaces { for (name, namespace) in modified_namespaces {
let namespace_folder = namespace.compile(&options, &compiler_state); let namespace_folder = namespace.compile(&options, &compiler_state);
data_folder.add_existing_folder(name, namespace_folder); data_folder.add_existing_folder(name, namespace_folder);
} }
@ -180,7 +276,7 @@ mod tests {
fn test_datapack() { fn test_datapack() {
let template_dir = tempfile::tempdir().expect("error creating tempdir"); let template_dir = tempfile::tempdir().expect("error creating tempdir");
let mut dp = Datapack::new(Datapack::LATEST_FORMAT) let mut dp = Datapack::new("main", Datapack::LATEST_FORMAT)
.with_description("My datapack") .with_description("My datapack")
.with_template_folder(template_dir.path()) .with_template_folder(template_dir.path())
.expect("error reading template folder"); .expect("error reading template folder");
@ -193,7 +289,7 @@ mod tests {
#[test] #[test]
fn test_generate_mcmeta() { fn test_generate_mcmeta() {
let dp = &Datapack::new(Datapack::LATEST_FORMAT).with_description("foo"); let dp = &Datapack::new("main", Datapack::LATEST_FORMAT).with_description("foo");
let state = Mutex::new(CompilerState::default()); let state = Mutex::new(CompilerState::default());
let mcmeta = generate_mcmeta(dp, &CompileOptions::default(), &state); let mcmeta = generate_mcmeta(dp, &CompileOptions::default(), &state);

View File

@ -28,9 +28,9 @@ pub struct Namespace {
impl Namespace { impl Namespace {
/// Create a new namespace. /// Create a new namespace.
pub(in crate::datapack) fn new(name: &str) -> Self { pub(in crate::datapack) fn new(name: impl Into<String>) -> Self {
Self { Self {
name: name.to_string(), name: name.into(),
functions: HashMap::new(), functions: HashMap::new(),
tags: HashMap::new(), tags: HashMap::new(),
} }
@ -62,23 +62,24 @@ impl Namespace {
/// Mutably get a function by name or create a new one if it doesn't exist. /// Mutably get a function by name or create a new one if it doesn't exist.
#[must_use] #[must_use]
pub fn function_mut(&mut self, name: &str) -> &mut Function { pub fn function_mut(&mut self, name: impl Into<String>) -> &mut Function {
let name = name.into();
self.functions self.functions
.entry(name.to_string()) .entry(name.clone())
.or_insert_with(|| Function::new(&self.name, name)) .or_insert_with(|| Function::new(&self.name, name))
} }
/// Get a tag by name and type. /// Get a tag by name and type.
#[must_use] #[must_use]
pub fn tag(&self, name: &str, tag_type: TagType) -> Option<&Tag> { pub fn tag(&self, name: impl Into<String>, tag_type: TagType) -> Option<&Tag> {
self.tags.get(&(name.to_string(), tag_type)) self.tags.get(&(name.into(), tag_type))
} }
/// Mutably get a tag by name and type or create a new one if it doesn't exist. /// Mutably get a tag by name and type or create a new one if it doesn't exist.
#[must_use] #[must_use]
pub fn tag_mut(&mut self, name: &str, tag_type: TagType) -> &mut Tag { pub fn tag_mut(&mut self, name: impl Into<String>, tag_type: TagType) -> &mut Tag {
self.tags self.tags
.entry((name.to_string(), tag_type)) .entry((name.into(), tag_type))
.or_insert_with(|| Tag::new(false)) .or_insert_with(|| Tag::new(false))
} }

View File

@ -37,10 +37,16 @@ impl Tag {
/// Get the values of the tag. /// Get the values of the tag.
#[must_use] #[must_use]
pub fn get_values(&self) -> &Vec<TagValue> { pub fn values(&self) -> &[TagValue] {
&self.values &self.values
} }
/// Get a mutable reference to the values of the tag.
#[must_use]
pub fn values_mut(&mut self) -> &mut Vec<TagValue> {
&mut self.values
}
/// Add a value to the tag. /// Add a value to the tag.
pub fn add_value(&mut self, value: TagValue) { pub fn add_value(&mut self, value: TagValue) {
self.values.push(value); self.values.push(value);
@ -186,7 +192,7 @@ mod tests {
required: true, required: true,
}); });
assert_eq!(tag.get_values().len(), 2); assert_eq!(tag.values().len(), 2);
let compiled = tag.compile(&CompileOptions::default(), &MutCompilerState::default()); let compiled = tag.compile(&CompileOptions::default(), &MutCompilerState::default());

View File

@ -10,6 +10,7 @@
)] )]
#![warn(clippy::all, clippy::pedantic, clippy::perf)] #![warn(clippy::all, clippy::pedantic, clippy::perf)]
#![allow(clippy::missing_panics_doc, clippy::missing_const_for_fn)] #![allow(clippy::missing_panics_doc, clippy::missing_const_for_fn)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod datapack; pub mod datapack;
pub mod util; pub mod util;

View File

@ -17,6 +17,8 @@ pub struct CompileOptions {
pub(crate) pack_format: u8, pub(crate) pack_format: u8,
/// Whether to compile in debug mode. /// Whether to compile in debug mode.
pub(crate) debug: bool, pub(crate) debug: bool,
/// Whether to generate an uninstall function.
pub(crate) uninstall_function: bool,
} }
impl CompileOptions { impl CompileOptions {
@ -25,6 +27,15 @@ impl CompileOptions {
pub fn with_debug(self, debug: bool) -> Self { pub fn with_debug(self, debug: bool) -> Self {
Self { debug, ..self } Self { debug, ..self }
} }
/// Set whether to generate an uninstall function.
#[must_use]
pub fn with_uninstall_function(self, uninstall_function: bool) -> Self {
Self {
uninstall_function,
..self
}
}
} }
impl Default for CompileOptions { impl Default for CompileOptions {
@ -32,6 +43,7 @@ impl Default for CompileOptions {
Self { Self {
pack_format: Datapack::LATEST_FORMAT, pack_format: Datapack::LATEST_FORMAT,
debug: true, debug: true,
uninstall_function: true,
} }
} }
} }

View File

@ -131,6 +131,7 @@ impl VFolder {
/// # Errors /// # Errors
/// - If the folder cannot be written /// - If the folder cannot be written
#[cfg(feature = "fs_access")] #[cfg(feature = "fs_access")]
#[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))]
pub fn place<P>(&self, path: P) -> std::io::Result<()> pub fn place<P>(&self, path: P) -> std::io::Result<()>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
@ -162,6 +163,7 @@ impl VFolder {
/// # Errors /// # Errors
/// - If the zip archive cannot be written /// - If the zip archive cannot be written
#[cfg(all(feature = "fs_access", feature = "zip"))] #[cfg(all(feature = "fs_access", feature = "zip"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "fs_access", feature = "zip"))))]
pub fn zip<P>(&self, path: P) -> std::io::Result<()> pub fn zip<P>(&self, path: P) -> std::io::Result<()>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
@ -174,6 +176,7 @@ impl VFolder {
/// # Errors /// # Errors
/// - If the zip archive cannot be written /// - If the zip archive cannot be written
#[cfg(all(feature = "fs_access", feature = "zip"))] #[cfg(all(feature = "fs_access", feature = "zip"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "fs_access", feature = "zip"))))]
pub fn zip_with_comment<P, S>(&self, path: P, comment: S) -> std::io::Result<()> pub fn zip_with_comment<P, S>(&self, path: P, comment: S) -> std::io::Result<()>
where where
P: AsRef<std::path::Path>, P: AsRef<std::path::Path>,
@ -246,14 +249,14 @@ impl VFolder {
/// Recursively merge another folder into this folder. /// Recursively merge another folder into this folder.
/// Returns a list of paths that were replaced by other. /// Returns a list of paths that were replaced by other.
pub fn merge(&mut self, other: Self) -> Vec<String> { pub fn merge(&mut self, other: Self) -> Vec<String> {
self._merge(other, "") self.merge_(other, "")
} }
fn _merge(&mut self, other: Self, prefix: &str) -> Vec<String> { fn merge_(&mut self, other: Self, prefix: &str) -> Vec<String> {
let mut replaced = Vec::new(); let mut replaced = Vec::new();
for (name, folder) in other.folders { for (name, folder) in other.folders {
if let Some(existing_folder) = self.folders.get_mut(&name) { if let Some(existing_folder) = self.folders.get_mut(&name) {
let replaced_folder = existing_folder._merge(folder, &format!("{prefix}{name}/")); let replaced_folder = existing_folder.merge_(folder, &format!("{prefix}{name}/"));
replaced.extend(replaced_folder); replaced.extend(replaced_folder);
} else { } else {
self.folders.insert(name, folder); self.folders.insert(name, folder);
@ -271,6 +274,7 @@ impl VFolder {
} }
#[cfg(feature = "fs_access")] #[cfg(feature = "fs_access")]
#[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))]
impl TryFrom<&std::path::Path> for VFolder { impl TryFrom<&std::path::Path> for VFolder {
type Error = std::io::Error; type Error = std::io::Error;
@ -374,6 +378,7 @@ impl From<&[u8]> for VFile {
} }
#[cfg(feature = "fs_access")] #[cfg(feature = "fs_access")]
#[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))]
impl TryFrom<&std::path::Path> for VFile { impl TryFrom<&std::path::Path> for VFile {
type Error = std::io::Error; type Error = std::io::Error;