oct 0.28.2

Octonary transcodings.
Documentation
// Copyright 2024-2026 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.

//! Raw fields for [`io::Error`].
//!
//! [`io::Error`]: crate::io::Error

use crate::io::{ErrorKind, SimpleError, UnpackedError};

#[cfg(feature = "alloc")]
use crate::io::CustomError;

#[cfg(feature = "std")]
use crate::io::RawOsError;

use core::fmt::{self, Debug, Formatter};
use core::num::NonZero;
use core::ptr::NonNull;

#[cfg(feature = "alloc")]
use alloc::boxed::Box;

#[cfg(feature = "std")]
const _: () = assert!(
	RawOsError::BITS < usize::BITS - Tag::BITS,
	"cannot encode `RawOsError` when overlapping with packed error tag",
);

// NOTE: This type is only possible on targets
// where `RawOsError` can fit into `NonNull<()>`
// without overlapping with the tag. This is not
// possible on targets where `RawOsError` aliases
// `i32` and never on UEFI where it aliases
// `usize`.
// NOTE: We like niche optimisations.
// SAFETY: We need to use some pointer type to keep
// track of provenance.
/// The raw, packed error data.
/// A packed input/output error.
#[derive(Clone, Copy)]
pub(super) struct PackedError(NonNull<()>);

impl PackedError {
	/// Constructs a new [`Simple`] error.
	///
	/// [`Simple`]: UnpackedError::Simple
	#[must_use]
	pub const fn new_simple(message: &'static SimpleError) -> Self {
 		// SAFETY: The alignement of `SimpleError` ensures
		// some always-zero padding bits, which equate to
		// `Tag::Simple`. This allows for us to avoid
		// manipulating the raw address, thus enabling this
		// operation in `const`. Lastly, the pointer
		// derives, which guarantees that it isn't null.
		let p = NonNull::from_ref(message).cast();
		Self(p)
	}

	/// Constructs a new [`Inline`] error.
	///
	/// [`Inline`]: UnpackedError::Inline
	#[must_use]
	pub fn new_inline(kind: ErrorKind) -> Self {
		let mut addr = 0usize;
		addr |= Tag::Inline.to_usize();
		addr |= kind.to_usize() << Tag::BITS;

		// SAFETY: `Tag::Inline` gives one bit.
		let addr = unsafe { NonZero::new_unchecked(addr) };

		// NOTE: We don't need any provenance as the
		// address will not be dereferenced.
		let p = NonNull::without_provenance(addr);
		Self(p)
	}

	/// Constructs a new [`Custom`] error.
	///
	/// [`Custom`]: UnpackedError::Custom
	#[cfg(feature = "alloc")]
	#[must_use]
	pub fn new_custom(custom: Box<CustomError>) -> Self {
		let mut p = Box::into_raw(custom).cast::<()>();

		p = unsafe { p.byte_add(Tag::Custom.to_usize()) };

		// SAFETY: `Tag::Custom` gives one bit, although
		// the original address couldn't've'd been null to
		// begin with.
		let p = unsafe { NonNull::new_unchecked(p) };
		Self(p)
	}

	/// Constructs a new [`Os`] error.
	///
	/// [`Os`]: UnpackedError::Os
	#[cfg(feature = "std")]
	#[must_use]
	pub fn new_os(raw: RawOsError) -> Self {
		#[cfg(target_pointer_width = "16")]
		compile_error!("cannot encode operating system errors on 16-bit platforms");

		#[allow(clippy::cast_possible_wrap)]
		let raw = raw.cast_unsigned() as usize;

		let mut addr = 0usize;
		addr |= Tag::Os.to_usize();
		addr |= raw << Tag::BITS;

		// SAFETY: `Tag::Os` gives one bit.
		let addr = unsafe { NonZero::new_unchecked(addr) };

		// NOTE: We don't need any provenance as the
		// address will not be dereferenced.
		let p = NonNull::without_provenance(addr);
		Self(p)
	}

	/// Unpacks the packed error.
	#[must_use]
	pub fn unpack(self) -> UnpackedError {
		match self.tag() {
			Tag::Simple => {
				// NOTE: `Tag::Simple` is zero and does not need
				// to be removed.
				let p = self.0.as_ptr().cast_const().cast::<SimpleError>();

				// SAFETY: Caller guarantees this.
				let message = unsafe { &*p };

				UnpackedError::Simple(message)
			}

			Tag::Inline => {
				let kind = self.as_usize() >> Tag::BITS;

				// SAFETY: Caller guarantees this.
				let kind = unsafe { ErrorKind::from_usize_unchecked(kind) };

				UnpackedError::Inline(kind)
			}

			#[cfg(feature = "alloc")]
			Tag::Custom => {
				let mut p = self.0.as_ptr().cast::<CustomError>();

				// SAFETY: The result can, at least, be null.
				p = unsafe { p.byte_sub(Tag::Custom.to_usize()) };

				UnpackedError::Custom(p)
			}

			#[cfg(feature = "std")]
			Tag::Os => {
				#[allow(clippy::cast_possible_truncation)]
				#[allow(clippy::cast_possible_wrap)]
				let raw = (self.as_usize() >> Tag::BITS) as RawOsError;

				UnpackedError::Os(raw)
			}
		}
	}

	/// Retrieves the data tag.
	#[inline]
	#[must_use]
	fn tag(self) -> Tag {
		const TAG_MASK: usize = !(!0 << Tag::BITS);

		let tag = self.as_usize() & TAG_MASK;

		match tag {
			0b00 => Tag::Simple,
			0b01 => Tag::Inline,

			#[cfg(feature = "alloc")]
			0b10 => Tag::Custom,

			#[cfg(feature = "std")]
			0b11 => Tag::Os,

			_ => unreachable!("tag should not be constructable"),
		}
	}

	/// Reinterprets the raw error as a [`usize`] value.
	#[inline]
	#[must_use]
	fn as_usize(self) -> usize {
		self.0.addr().get()
	}
}

impl Debug for PackedError {
	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
		Debug::fmt(&self.unpack(), f)
	}
}

// SAFETY: All PackedError fields must be `Send` as well.
unsafe impl Send for PackedError {}

// SAFETY: All PackedError fields must be `Sync` as well.
unsafe impl Sync for PackedError {}

/// A tag for the raw error data.
#[repr(usize)]
#[derive(
	Clone,
	Copy,
	Debug,
	Eq,
	Hash,
	PartialEq,
)]
enum Tag {
	/// A [`Simple`] error.
	///
	/// [`Simple`]: UnpackedError::Simple
	Simple = 0b00,

	/// A [`Inline`] error.
	///
	/// [`Inline`]: UnpackedError::Inline
	Inline = 0b01,

	/// A [`Custom`] error.
	///
	/// [`Custom`]: UnpackedError::Custom
	#[cfg(feature = "alloc")]
	Custom = 0b10,

	/// A [`Os`] error.
	///
	/// [`Os`]: UnpackedError::Os
	#[cfg(feature = "std")]
	Os = 0b11,
}

impl Tag {
	/// The amount of bits in any tag.
	pub const BITS: u32 = 2;

	/// Converts the tag kind into a [`usize`] value.
	#[inline(always)]
	#[must_use]
	pub(crate) const fn to_usize(self) -> usize {
		self as usize
	}
}