diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 099e07f4..96d54cea 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -14,7 +14,7 @@ jobs: - name: Generate code coverage run: | - cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + cargo +nightly tarpaulin --verbose --features debug --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v4 diff --git a/.github/workflows/rustbench.yml b/.github/workflows/rustbench.yml index b31d718d..2ab45ab4 100644 --- a/.github/workflows/rustbench.yml +++ b/.github/workflows/rustbench.yml @@ -29,7 +29,9 @@ jobs: version: latest - name: Run Benchmarks on changes - run: cargo bench --workspace --bench bench -- --save-baseline changes + run: | + cargo bench --workspace --bench bench -- --save-baseline default_changes + cargo bench --workspace --bench bench --features forbid_unsafe -- --save-baseline forbid_unsafe_changes - uses: actions/checkout@v4 with: @@ -37,24 +39,49 @@ jobs: clean: false - name: Run Benchmarks before changes - run: cargo bench --workspace --bench bench -- --save-baseline before + run: | + cargo bench --workspace --bench bench -- --save-baseline default_before + + # Skip benchmarking for forbid_unsafe feature if the PR base commit doesn't have the feature + if [[ "$(grep forbid_unsafe Cargo.toml)" ]]; then + cargo bench --workspace --bench bench --features forbid_unsafe -- --save-baseline forbid_unsafe_before + fi - name: Compare benchmarks run: | echo 'results<> $GITHUB_OUTPUT - critcmp before changes >> $GITHUB_OUTPUT + critcmp default_before default_changes >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT id: compare + - name: Compare benchmarks for forbid-unsafe + run: | + echo 'results<> $GITHUB_OUTPUT + if [[ "$(grep forbid_unsafe Cargo.toml)" ]]; then + critcmp forbid_unsafe_before forbid_unsafe_changes >> $GITHUB_OUTPUT + else + echo 'NOTE: PR base commit does not support the "forbid_unsafe" feature;' >> $GITHUB_OUTPUT + echo 'comparing forbid_unsafe against the default features on base instead.' >> $GITHUB_OUTPUT + critcmp default_before forbid_unsafe_changes >> $GITHUB_OUTPUT + fi + echo 'EOF' >> $GITHUB_OUTPUT + id: compare-forbid-unsafe + - name: Comment PR with benchmarks uses: thollander/actions-comment-pull-request@v2 continue-on-error: true with: message: | - Benchmark results: + Benchmark results with default features: ``` ${{ steps.compare.outputs.results }} ``` + + Benchmark results with feature "forbid_unsafe": + ``` + ${{ steps.compare-forbid-unsafe.outputs.results }} + ``` + comment_tag: benchmarks id: comment @@ -66,3 +93,8 @@ jobs: echo '```' >> $GITHUB_STEP_SUMMARY echo '${{ steps.compare.outputs.results }}' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + + echo '### Benchmark results with forbid-unsafe' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '${{ steps.compare-forbid-unsafe.outputs.results }}' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/rustdoc.yml b/.github/workflows/rustdoc.yml index 9134249a..97851954 100644 --- a/.github/workflows/rustdoc.yml +++ b/.github/workflows/rustdoc.yml @@ -22,4 +22,4 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Check rustdoc build - run: RUSTDOCFLAGS='--cfg docsrs' cargo +nightly doc --all-features -Zunstable-options -Zrustdoc-scrape-examples + run: RUSTDOCFLAGS='--cfg docsrs' cargo +nightly doc --features debug -Zunstable-options -Zrustdoc-scrape-examples diff --git a/.github/workflows/rustlib.yml b/.github/workflows/rustlib.yml index 4152f2d5..69c2b717 100644 --- a/.github/workflows/rustlib.yml +++ b/.github/workflows/rustlib.yml @@ -22,7 +22,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Check rustdoc build - run: RUSTDOCFLAGS='--cfg docsrs' cargo +nightly doc --all-features -Zunstable-options -Zrustdoc-scrape-examples + run: RUSTDOCFLAGS='--cfg docsrs' cargo +nightly doc --features debug -Zunstable-options -Zrustdoc-scrape-examples tests: name: Tests @@ -36,6 +36,9 @@ jobs: - macos-latest - ubuntu-latest - windows-latest + features: + - "" # default features + - "--features forbid_unsafe" runs-on: ${{ matrix.os }} steps: @@ -52,4 +55,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --workspace --verbose + args: --workspace --verbose ${{ matrix.features }} diff --git a/.github/workflows/rustlints.yml b/.github/workflows/rustlints.yml index 53e9d6ff..db3d6a9f 100644 --- a/.github/workflows/rustlints.yml +++ b/.github/workflows/rustlints.yml @@ -21,7 +21,7 @@ jobs: components: clippy - name: Check clippy - run: cargo clippy --all-features -- -D warnings + run: cargo clippy --features debug -- -D warnings rustfmt: name: Rustfmt diff --git a/.github/workflows/rustmsrv.yml b/.github/workflows/rustmsrv.yml index d49650ca..e5002579 100644 --- a/.github/workflows/rustmsrv.yml +++ b/.github/workflows/rustmsrv.yml @@ -24,4 +24,4 @@ jobs: version: ^0.15.1 - name: Check MSRV - run: cargo msrv verify -- cargo check --workspace --all-features + run: cargo msrv verify -- cargo check --workspace --features debug diff --git a/Cargo.toml b/Cargo.toml index 7911effd..6dae4b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,9 +51,11 @@ default = ["export_derive", "std"] export_derive = ["logos-derive"] # Should the crate use the standard library? std = [] +# Use safe alternatives for unsafe code (may impact performance)? +forbid_unsafe = ["logos-derive?/forbid_unsafe"] [package.metadata.docs.rs] -all-features = true +features = ["debug"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] rustdoc-args = ["--cfg", "docsrs"] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 38ce439d..abbde79c 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -11,6 +11,7 @@ + [Using callbacks](./callbacks.md) + [Common regular expressions](./common-regex.md) + [Debugging](./debugging.md) ++ [Unsafe Code](./unsafe.md) + [Examples](./examples.md) + [Brainfuck interpreter](./examples/brainfuck.md) + [JSON parser](./examples/json.md) diff --git a/book/src/contributing/setup.md b/book/src/contributing/setup.md index f9e17fe2..5aa4c779 100644 --- a/book/src/contributing/setup.md +++ b/book/src/contributing/setup.md @@ -81,7 +81,7 @@ configuration to the one used by [docs.rs](https://docs.rs/logos/latest/logos/): ```bash RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc \ - --all-features \ + --features debug \ -Zunstable-options \ -Zrustdoc-scrape-examples \ --no-deps \ diff --git a/book/src/unsafe.md b/book/src/unsafe.md new file mode 100644 index 00000000..a6056f04 --- /dev/null +++ b/book/src/unsafe.md @@ -0,0 +1,36 @@ +# Unsafe Code + +By default, **Logos** uses unsafe code to avoid unnecessary bounds checks while +accessing slices of the input `Source`. + +This unsafe code also exists in the code generated by the `Logos` derive macro, +which generates a deterministic finite automata (DFA). Reasoning about the correctness +of this generated code can be difficult - if the derivation of the DFA in `Logos` +is correct, then this generated code will be correct and any mistakes in implementation +would be caught given sufficient fuzz testing. + +Use of unsafe code is the default as this typically provides the fastest parser. + +## Disabling Unsafe Code + +However, for applications accepting untrusted input in a trusted context, this +may not be a sufficient correctness justification. + +For those applications which cannot tolerate unsafe code, the feature `forbid-unsafe` +may be enabled. This replaces unchecked accesses in the `Logos` crate with safe, +checked alternatives which will panic on out-of-bounds access rather than cause +undefined behavior. Additionally, code generated by the macro will not use the +unsafe keyword, so generated code may be used in a crates using the +`#![forbid(unsafe_code)]` attribute. + +When the `forbid-unsafe` feature is added to a direct dependency on the `Logos` crate, +[Feature Unification](https://doc.rust-lang.org/cargo/reference/features.html#feature-unification) +ensures any transitive inclusion of `Logos` via other dependencies also have unsafe +code disabled. + +Generally, disabling unsafe code will result in a slower parser. + +However making definitive statements around performance of safe-only code is difficult, +as there are too many variables to consider between compiler optimizations, +the specific grammar being parsed, and the target processor. The automated benchmarks +of this crate show around a 10% slowdown in safe-only code at the time of this writing. diff --git a/logos-codegen/Cargo.toml b/logos-codegen/Cargo.toml index 0f689164..9b27679f 100644 --- a/logos-codegen/Cargo.toml +++ b/logos-codegen/Cargo.toml @@ -16,6 +16,8 @@ rstest = "0.18.2" debug = [] # Exports out internal methods for fuzzing fuzzing = [] +# Don't use or generate unsafe code +forbid_unsafe = [] [lib] bench = false diff --git a/logos-codegen/src/generator/context.rs b/logos-codegen/src/generator/context.rs index 82f76f39..dc52f594 100644 --- a/logos-codegen/src/generator/context.rs +++ b/logos-codegen/src/generator/context.rs @@ -59,12 +59,20 @@ impl Context { self.available.saturating_sub(self.at) } - pub fn read_byte_unchecked(&mut self) -> TokenStream { + pub fn read_byte(&mut self) -> TokenStream { let at = self.at; self.advance(1); - quote!(lex.read_byte_unchecked(#at)) + #[cfg(not(feature = "forbid_unsafe"))] + { + quote!(unsafe { lex.read_byte_unchecked(#at) }) + } + + #[cfg(feature = "forbid_unsafe")] + { + quote!(lex.read_byte(#at)) + } } pub fn read(&mut self, len: usize) -> TokenStream { diff --git a/logos-codegen/src/generator/fork.rs b/logos-codegen/src/generator/fork.rs index db0e99e9..0f8eeec2 100644 --- a/logos-codegen/src/generator/fork.rs +++ b/logos-codegen/src/generator/fork.rs @@ -136,9 +136,9 @@ impl<'a> Generator<'a> { let min_read = self.meta[this].min_read; if ctx.remainder() >= max(min_read, 1) { - let read = ctx.read_byte_unchecked(); + let read = ctx.read_byte(); - return (quote!(byte), quote!(let byte = unsafe { #read };)); + return (quote!(byte), quote!(let byte = #read;)); } match min_read { diff --git a/logos-derive/Cargo.toml b/logos-derive/Cargo.toml index d300de27..6c1b170b 100644 --- a/logos-derive/Cargo.toml +++ b/logos-derive/Cargo.toml @@ -4,6 +4,8 @@ logos-codegen = {version = "0.14.1", path = "../logos-codegen"} [features] # Enables debug messages debug = ["logos-codegen/debug"] +# Don't use or generate unsafe code +forbid_unsafe = ["logos-codegen/forbid_unsafe"] [lib] bench = false diff --git a/src/internal.rs b/src/internal.rs index 2957fede..e1a20de3 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -17,8 +17,13 @@ pub trait LexerInternal<'source> { fn read_at>(&self, n: usize) -> Option; /// Unchecked read a byte at current position, offset by `n`. + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, n: usize) -> u8; + /// Checked read a byte at current position, offset by `n`. + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, n: usize) -> u8; + /// Test a chunk at current position with a closure. fn test, F: FnOnce(T) -> bool>(&self, test: F) -> bool; diff --git a/src/lexer.rs b/src/lexer.rs index f8db4ce3..22fd3b5a 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -3,7 +3,6 @@ use super::Logos; use crate::source::{self, Source}; use core::fmt::{self, Debug}; -use core::mem::ManuallyDrop; use core::ops::{Deref, DerefMut}; /// Byte range in the source. @@ -13,7 +12,12 @@ pub type Span = core::ops::Range; /// `Source` and produce tokens for enums implementing the `Logos` trait. pub struct Lexer<'source, Token: Logos<'source>> { source: &'source Token::Source, - token: ManuallyDrop>>, + + #[cfg(not(feature = "forbid_unsafe"))] + token: core::mem::ManuallyDrop>>, + #[cfg(feature = "forbid_unsafe")] + token: Option>, + token_start: usize, token_end: usize, @@ -54,7 +58,7 @@ impl<'source, Token: Logos<'source>> Lexer<'source, Token> { pub fn with_extras(source: &'source Token::Source, extras: Token::Extras) -> Self { Lexer { source, - token: ManuallyDrop::new(None), + token: Default::default(), extras, token_start: 0, token_end: 0, @@ -140,16 +144,33 @@ impl<'source, Token: Logos<'source>> Lexer<'source, Token> { /// Get a string slice of the current token. #[inline] pub fn slice(&self) -> ::Slice<'source> { - unsafe { self.source.slice_unchecked(self.span()) } + // SAFETY: in bounds if `token_start` and `token_end` are in bounds. + // * `token_start` is initially zero and is set to `token_end` in `next`, so + // it remains in bounds as long as `token_end` remains in bounds. + // * `token_end` is initially zero and is only incremented in `bump`. `bump` + // will panic if `Source::is_boundary` is false. + // * Thus safety is contingent on the correct implementation of the `is_boundary` + // method. + #[cfg(not(feature = "forbid_unsafe"))] + unsafe { + self.source.slice_unchecked(self.span()) + } + #[cfg(feature = "forbid_unsafe")] + self.source.slice(self.span()).unwrap() } /// Get a slice of remaining source, starting at the end of current token. #[inline] pub fn remainder(&self) -> ::Slice<'source> { + #[cfg(not(feature = "forbid_unsafe"))] unsafe { self.source .slice_unchecked(self.token_end..self.source.len()) } + #[cfg(feature = "forbid_unsafe")] + self.source + .slice(self.token_end..self.source.len()) + .unwrap() } /// Turn this lexer into a lexer for a new token type. @@ -163,7 +184,7 @@ impl<'source, Token: Logos<'source>> Lexer<'source, Token> { { Lexer { source: self.source, - token: ManuallyDrop::new(None), + token: Default::default(), extras: self.extras.into(), token_start: self.token_start, token_end: self.token_end, @@ -194,7 +215,7 @@ where fn clone(&self) -> Self { Lexer { extras: self.extras.clone(), - token: ManuallyDrop::new(None), + token: Default::default(), ..*self } } @@ -216,7 +237,14 @@ where // Since we always immediately return a newly set token here, // we don't have to replace it with `None` or manually drop // it later. - unsafe { ManuallyDrop::take(&mut self.token) } + #[cfg(not(feature = "forbid_unsafe"))] + unsafe { + core::mem::ManuallyDrop::take(&mut self.token) + } + #[cfg(feature = "forbid_unsafe")] + { + self.token.take() + } } } @@ -302,10 +330,17 @@ where } #[inline] + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, n: usize) -> u8 { self.source.read_byte_unchecked(self.token_end + n) } + #[inline] + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, n: usize) -> u8 { + self.source.read_byte(self.token_end + n) + } + /// Test a chunk at current position with a closure. #[inline] fn test(&self, test: F) -> bool @@ -341,12 +376,19 @@ where #[inline] fn error(&mut self) { self.token_end = self.source.find_boundary(self.token_end); - self.token = ManuallyDrop::new(Some(Err(Token::Error::default()))); + #[cfg(not(feature = "forbid_unsafe"))] + { + self.token = core::mem::ManuallyDrop::new(Some(Err(Token::Error::default()))); + } + #[cfg(feature = "forbid_unsafe")] + { + self.token = Some(Err(Token::Error::default())); + } } #[inline] fn end(&mut self) { - self.token = ManuallyDrop::new(None); + self.token = Default::default(); } #[inline] @@ -357,6 +399,13 @@ where <>::Token as Logos<'source>>::Error, >, ) { - self.token = ManuallyDrop::new(Some(token)); + #[cfg(not(feature = "forbid_unsafe"))] + { + self.token = core::mem::ManuallyDrop::new(Some(token)); + } + #[cfg(feature = "forbid_unsafe")] + { + self.token = Some(token) + } } } diff --git a/src/lib.rs b/src/lib.rs index 41bf7dc6..6d58a490 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs)] #![doc(html_logo_url = "https://maciej.codes/kosz/logos.png")] +#![cfg_attr(feature = "forbid_unsafe", forbid(unsafe_code))] extern crate core; diff --git a/src/source.rs b/src/source.rs index b1a114eb..37e5c6a4 100644 --- a/src/source.rs +++ b/src/source.rs @@ -12,6 +12,11 @@ use core::ops::{Deref, Range}; /// Most notably this is implemented for `&str`. It is unlikely you will /// ever want to use this Trait yourself, unless implementing a new `Source` /// the `Lexer` can use. +/// +/// SAFETY: Unless the unsafe functions of this trait are disabled with the `forbid_unsafe` +/// feature, the correctness of the unsafe functions of this trait depend on the correct +/// implementation of the `len` and `find_boundary` functions so generated code does not request +/// out-of-bounds access. #[allow(clippy::len_without_is_empty)] pub trait Source { /// A type this `Source` can be sliced into. @@ -48,8 +53,13 @@ pub trait Source { /// # Safety /// /// Offset should not exceed bounds. + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, offset: usize) -> u8; + /// Read a byte with bounds checking. + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, offset: usize) -> u8; + /// Get a slice of the source at given range. This is analogous to /// `slice::get(range)`. /// @@ -77,6 +87,7 @@ pub trait Source { /// assert_eq!(::slice_unchecked(&foo, 51..59), "Eschaton"); /// } /// ``` + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn slice_unchecked(&self, range: Range) -> Self::Slice<'_>; /// For `&str` sources attempts to find the closest `char` boundary at which source @@ -108,24 +119,36 @@ impl Source for str { where Chunk: self::Chunk<'a>, { + #[cfg(not(feature = "forbid_unsafe"))] if offset + (Chunk::SIZE - 1) < self.len() { // # Safety: we just performed a bound check. Some(unsafe { Chunk::from_ptr(self.as_ptr().add(offset)) }) } else { None } + + #[cfg(feature = "forbid_unsafe")] + Chunk::from_slice(self.as_bytes().slice(offset..Chunk::SIZE + offset)?) } #[inline] + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, offset: usize) -> u8 { Chunk::from_ptr(self.as_ptr().add(offset)) } + #[inline] + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, offset: usize) -> u8 { + self.as_bytes()[offset] + } + #[inline] fn slice(&self, range: Range) -> Option<&str> { self.get(range) } + #[cfg(not(feature = "forbid_unsafe"))] #[inline] unsafe fn slice_unchecked(&self, range: Range) -> &str { debug_assert!( @@ -166,23 +189,35 @@ impl Source for [u8] { where Chunk: self::Chunk<'a>, { + #[cfg(not(feature = "forbid_unsafe"))] if offset + (Chunk::SIZE - 1) < self.len() { Some(unsafe { Chunk::from_ptr(self.as_ptr().add(offset)) }) } else { None } + + #[cfg(feature = "forbid_unsafe")] + Chunk::from_slice(self.slice(offset..Chunk::SIZE + offset)?) } #[inline] + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, offset: usize) -> u8 { Chunk::from_ptr(self.as_ptr().add(offset)) } + #[inline] + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, offset: usize) -> u8 { + self[offset] + } + #[inline] fn slice(&self, range: Range) -> Option<&[u8]> { self.get(range) } + #[cfg(not(feature = "forbid_unsafe"))] #[inline] unsafe fn slice_unchecked(&self, range: Range) -> &[u8] { debug_assert!( @@ -220,14 +255,21 @@ where self.deref().read(offset) } + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, offset: usize) -> u8 { self.deref().read_byte_unchecked(offset) } + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, offset: usize) -> u8 { + self.deref().read_byte(offset) + } + fn slice(&self, range: Range) -> Option> { self.deref().slice(range) } + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn slice_unchecked(&self, range: Range) -> Self::Slice<'_> { self.deref().slice_unchecked(range) } @@ -253,23 +295,43 @@ pub trait Chunk<'source>: Sized + Copy + PartialEq + Eq { /// # Safety /// /// Raw byte pointer should point to a valid location in source. + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn from_ptr(ptr: *const u8) -> Self; + + /// Create a chunk from a slice. + /// Returns None if the slice is not long enough to produce the chunk. + #[cfg(feature = "forbid_unsafe")] + fn from_slice(s: &'source [u8]) -> Option; } impl<'source> Chunk<'source> for u8 { const SIZE: usize = 1; #[inline] + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn from_ptr(ptr: *const u8) -> Self { *ptr } + + #[inline] + #[cfg(feature = "forbid_unsafe")] + fn from_slice(s: &'source [u8]) -> Option { + s.first().copied() + } } impl<'source, const N: usize> Chunk<'source> for &'source [u8; N] { const SIZE: usize = N; #[inline] + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn from_ptr(ptr: *const u8) -> Self { &*(ptr as *const [u8; N]) } + + #[inline] + #[cfg(feature = "forbid_unsafe")] + fn from_slice(s: &'source [u8]) -> Option { + s.slice(0..Self::SIZE).and_then(|x| x.try_into().ok()) + } } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 54959f13..3cffb388 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -8,6 +8,12 @@ version = "0.1.0" logos-derive = {path = "../logos-derive"} logos = {path = "../", default-features = false, features = ["std"]} +[features] +forbid_unsafe = [ + "logos-derive/forbid_unsafe", + "logos/forbid_unsafe" +] + [dev-dependencies] criterion = "0.4" diff --git a/tests/tests/source.rs b/tests/tests/source.rs index c747a354..c31d2d28 100644 --- a/tests/tests/source.rs +++ b/tests/tests/source.rs @@ -19,14 +19,21 @@ impl<'s, S: ?Sized + Source> Source for RefSource<'s, S> { self.0.read(offset) } + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn read_byte_unchecked(&self, offset: usize) -> u8 { self.0.read_byte_unchecked(offset) } + #[cfg(feature = "forbid_unsafe")] + fn read_byte(&self, offset: usize) -> u8 { + self.0.read_byte(offset) + } + fn slice(&self, range: Range) -> Option> { self.0.slice(range) } + #[cfg(not(feature = "forbid_unsafe"))] unsafe fn slice_unchecked(&self, range: Range) -> Self::Slice<'_> { self.0.slice_unchecked(range) }