Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["codegen", "tools/create-data-file", "tools/dump-data-file"]

[package]
name = "data_bucket"
version = "0.5.7"
version = "0.6.0"
edition = "2021"
authors = ["Handy-caT"]
license = "MIT"
Expand All @@ -13,7 +13,6 @@ description = "DataBucket is container for WorkTable's data"
[dependencies]
data_bucket_derive = { path = "codegen", version = "^0.3" }

eyre = "0.6.12"
derive_more = { version = "1.0.0", features = ["from", "error", "display", "into"] }
rkyv = { version = "0.8.17", features = ["uuid-1"] }
uuid = { version = "1.11.0", features = ["v4"] }
Expand Down
154 changes: 154 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! What this crate can refuse on.
//!
//! # Why this exists rather than `eyre`
//!
//! `eyre::Report` is a `std` type, and it was in the return type of every
//! fallible function here, so the whole crate reached `std` through its own
//! signatures. Nothing else about page framing needs an operating system: the
//! layout is bytes, the checks are arithmetic. This is the type that lets the
//! rest of the crate say so.
//!
//! It is also more useful than a formatted string. A caller that wants to
//! distinguish "this page is full" from "these bytes are damaged" could not,
//! because both arrived as a `Report` carrying prose.
//!
//! Consumers keep working: `Error` implements `core::error::Error`, so `?`
//! into an `eyre::Result` converts exactly as it did before.

use core::fmt::{Display, Formatter, Result as FmtResult};

use crate::page::PageId;

/// The result of anything in this crate that can fail.
pub type Result<T> = core::result::Result<T, Error>;

/// A refusal, with the numbers that justify it.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Error {
/// A write's length does not match the link it was given.
///
/// A `Link` names an exact byte range, so a write of another size is not
/// a resize: it is a write that would land on the neighbouring row.
LinkLengthMismatch {
/// The length the link reserves.
expected: u32,
/// The length the caller supplied.
found: usize,
},
/// A link's range runs past the end of the page it points into.
///
/// Summed in `u64`, because `offset + length` in `u32` can wrap past 4 GiB
/// and slip under the bound.
LinkOutOfBounds {
/// Where the link starts.
offset: u32,
/// How far it runs.
length: u32,
/// How much the page holds.
capacity: usize,
},
/// A page's contents do not fit the page.
///
/// Raised where the write is prepared rather than where it lands, so an
/// over-budget page fails in its own persist instead of quietly writing
/// into its neighbour.
PageOverflow {
/// The page whose write is over budget.
page: PageId,
/// Bytes the write needs.
needed: usize,
/// Bytes available.
capacity: usize,
},
/// Bytes that should have been a structure were not one.
///
/// The label says which structure, because "corrupt" on its own does not
/// tell an operator which part of a file to distrust.
Corrupt {
/// What failed to parse.
what: &'static str,
},
/// A value would not archive.
///
/// rkyv reports this when its allocator refuses, which on this path means
/// the process is already out of memory.
Encode,
/// The file would not answer.
///
/// Held as the raw OS code rather than an `io::Error`, because that type
/// is `std` and this one is the reason the crate stops needing it. The
/// code is what an operator acts on; the message that came with it says
/// nothing the code does not.
Io {
/// `raw_os_error`, when the failure came from the operating system.
code: Option<i32>,
},
/// A change-data event arrived that this page cannot apply.
///
/// `SplitNode`, `CreateNode` and `RemoveNode` change which pages exist,
/// which is the caller's business rather than one page's.
UnapplicableEvent,
}

impl Display for Error {
fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
match self {
Self::LinkLengthMismatch { expected, found } => write!(
formatter,
"a {found} byte write does not match its {expected} byte link"
),
Self::LinkOutOfBounds {
offset,
length,
capacity,
} => write!(
formatter,
"a link at {offset} running {length} bytes leaves a {capacity} byte page"
),
Self::PageOverflow {
page,
needed,
capacity,
} => write!(
formatter,
"page {page:?} needs {needed} bytes of a {capacity} byte page"
),
Self::Corrupt { what } => write!(formatter, "torn or corrupt {what}"),
Self::Encode => write!(formatter, "a value would not archive"),
Self::Io { code: Some(code) } => write!(formatter, "the file failed, os error {code}"),
Self::Io { code: None } => write!(formatter, "the file failed"),
Self::UnapplicableEvent => write!(
formatter,
"events of `SplitNode`, `CreateNode` or `RemoveNode` cannot be applied to a page"
),
}
}
}

impl core::error::Error for Error {}

// Not gated yet: the crate still reaches the file system directly. When that
// moves behind a trait this impl goes with it.
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io {
code: error.raw_os_error(),
}
}
}

impl From<rkyv::rancor::Error> for Error {
fn from(_: rkyv::rancor::Error) -> Self {
Self::Encode
}
}

impl From<crate::page::PageOverflowError> for Error {
fn from(error: crate::page::PageOverflowError) -> Self {
Self::PageOverflow {
page: error.page_id,
needed: error.data_length,
capacity: error.capacity,
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ extern crate core;
// uses its own derive: alias ourselves so the generated code resolves here too.
extern crate self as data_bucket;

pub mod error;
pub mod link;
pub mod page;
pub mod persistence;
Expand Down
69 changes: 40 additions & 29 deletions src/page/data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::{Error, Result};
use crate::Link;
use crate::Persistable;
use eyre::{eyre, Result};

#[derive(Debug)]
pub struct DataPage<const DATA_LENGTH: usize> {
Expand All @@ -11,24 +11,22 @@ pub struct DataPage<const DATA_LENGTH: usize> {
impl<const DATA_LENGTH: usize> DataPage<DATA_LENGTH> {
pub fn update_at(&mut self, link: Link, new_data: &[u8]) -> Result<()> {
if new_data.len() as u32 != link.length {
return Err(eyre!(
"New data length {} does not match link length {}",
new_data.len(),
link.length
));
return Err(Error::LinkLengthMismatch {
expected: link.length,
found: new_data.len(),
});
}

// Sum in usize: `offset + length` in u32 can wrap past 4 GiB and
// slip under the bound with a range that is actually out of page.
let start = link.offset as usize;
let end = link.offset as usize + link.length as usize;
if end > DATA_LENGTH {
return Err(eyre!(
"Link range (offset: {}, length: {}) exceeds data bounds ({})",
link.offset,
link.length,
DATA_LENGTH
));
return Err(Error::LinkOutOfBounds {
offset: link.offset,
length: link.length,
capacity: DATA_LENGTH,
});
}

self.data[start..end].copy_from_slice(new_data);
Expand All @@ -42,12 +40,11 @@ impl<const DATA_LENGTH: usize> DataPage<DATA_LENGTH> {
let start = link.offset as usize;
let end = link.offset as usize + link.length as usize;
if end > DATA_LENGTH {
return Err(eyre!(
"Link range (offset: {}, length: {}) exceeds data bounds ({})",
link.offset,
link.length,
DATA_LENGTH
));
return Err(Error::LinkOutOfBounds {
offset: link.offset,
length: link.length,
capacity: DATA_LENGTH,
});
}

Ok(&self.data[start..end])
Expand Down Expand Up @@ -105,9 +102,13 @@ mod tests {
};

let err = data.update_at(link, &[1, 2]).unwrap_err();
assert!(err
.to_string()
.contains("New data length 2 does not match link length 3"));
assert_eq!(
err,
Error::LinkLengthMismatch {
expected: 3,
found: 2
}
);
}

#[test]
Expand All @@ -124,9 +125,14 @@ mod tests {
};

let err = data.update_at(link, &[1, 2, 3]).unwrap_err();
assert!(err
.to_string()
.contains("Link range (offset: 98, length: 3) exceeds data bounds (100)"));
assert_eq!(
err,
Error::LinkOutOfBounds {
offset: 98,
length: 3,
capacity: 100
}
);
}

#[test]
Expand All @@ -145,7 +151,7 @@ mod tests {
};

let err = data.update_at(link, &[1, 2, 3, 4, 5, 6, 7, 8]).unwrap_err();
assert!(err.to_string().contains("exceeds data bounds"));
assert!(matches!(err, Error::LinkOutOfBounds { .. }));
}

#[test]
Expand All @@ -162,7 +168,7 @@ mod tests {
};

let err = data.get_at(link).unwrap_err();
assert!(err.to_string().contains("exceeds data bounds"));
assert!(matches!(err, Error::LinkOutOfBounds { .. }));
}

#[test]
Expand All @@ -179,8 +185,13 @@ mod tests {
};

let err = data.get_at(link).unwrap_err();
assert!(err
.to_string()
.contains("Link range (offset: 98, length: 3) exceeds data bounds (100)"));
assert_eq!(
err,
Error::LinkOutOfBounds {
offset: 98,
length: 3,
capacity: 100
}
);
}
}
13 changes: 6 additions & 7 deletions src/page/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use rkyv::{Archive, Deserialize, Serialize};
use tokio::fs::File;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};

use crate::page::PageOverflowError;
use crate::{
align, align_to, seek_to_page_start, Link, Persistable, SizeMeasurable, VariableSizeMeasurable,
GENERAL_HEADER_SIZE, INNER_PAGE_SIZE,
Expand All @@ -33,24 +32,24 @@ pub trait IndexPageUtility<T> {
fn parse_index_page_utility(
file: &mut File,
page_id: PageId,
) -> impl std::future::Future<Output = eyre::Result<Self::Utility>> + Send;
) -> impl std::future::Future<Output = crate::error::Result<Self::Utility>> + Send;

fn persist_index_page_utility(
file: &mut File,
page_id: PageId,
utility: Self::Utility,
) -> impl std::future::Future<Output = eyre::Result<()>> + Send {
) -> impl std::future::Future<Output = crate::error::Result<()>> + Send {
async move {
let bytes = utility.as_bytes();
let utility_length = bytes.as_ref().len();
// An oversized utility must fail here, in its own persist,
// instead of writing past the page slot into the neighbor page.
if utility_length > INNER_PAGE_SIZE {
return Err(eyre::Report::new(PageOverflowError {
page_id,
data_length: utility_length,
return Err(crate::error::Error::PageOverflow {
page: page_id,
needed: utility_length,
capacity: INNER_PAGE_SIZE,
}));
});
}
seek_to_page_start(file, page_id.0).await?;
file.seek(SeekFrom::Current(GENERAL_HEADER_SIZE as i64))
Expand Down
Loading
Loading