hawdb-storage 0.13.0

An embedded Rust graph database for local data-plane workloads
Documentation
// Copyright 2026 Nowledge
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Positioned file reads shared by storage and other internal workspace crates.

use std::fs::File;
use std::io::{self, ErrorKind};

/// Fill a buffer from an absolute file offset, retrying interrupted short reads.
///
/// The range must have a representable exclusive end. On error, the buffer may
/// contain a partial read and must not be consumed as a complete payload.
/// Unix preserves the file cursor; Windows uses `seek_read`, which moves it.
pub fn read_exact_at(
    file: &impl PositionedReadFile,
    buffer: &mut [u8],
    offset: u64,
) -> io::Result<()> {
    file.read_exact_at(buffer, offset)
}

/// Read a managed file while admitting any cold immutable identity validation.
/// Returns the validation bytes in addition to the selected buffer's length.
pub(crate) fn read_exact_at_admitted(
    file: &crate::file_io::File,
    buffer: &mut [u8],
    offset: u64,
    admit_validation: impl FnMut(u64) -> io::Result<()>,
    admit_page: impl FnOnce() -> io::Result<()>,
) -> io::Result<u64> {
    file.with_native_admitted(admit_validation, |native| {
        // Acquire the native lease first: descriptor admission must not be
        // mistaken for payload I/O. Charge the slot before its first read.
        admit_page()?;
        read_native_exact_at(native, buffer, offset)
    })
    .map(|((), validation_bytes)| validation_bytes)
}

/// Internal positioned IO contract shared by native and admitted file handles.
pub trait PositionedReadFile {
    fn read_exact_at(&self, buffer: &mut [u8], offset: u64) -> io::Result<()>;
}

impl<T: PositionedReadFile + ?Sized> PositionedReadFile for std::sync::Arc<T> {
    fn read_exact_at(&self, buffer: &mut [u8], offset: u64) -> io::Result<()> {
        (**self).read_exact_at(buffer, offset)
    }
}

impl PositionedReadFile for File {
    fn read_exact_at(&self, buffer: &mut [u8], offset: u64) -> io::Result<()> {
        read_native_exact_at(self, buffer, offset)
    }
}

impl PositionedReadFile for crate::file_io::File {
    fn read_exact_at(&self, buffer: &mut [u8], offset: u64) -> io::Result<()> {
        self.with_native(|file| read_native_exact_at(file, buffer, offset))
    }
}

/// Perform one positioned read; the caller controls short-read/EINTR retries.
pub(crate) fn read_native_at_once(
    file: &File,
    buffer: &mut [u8],
    offset: u64,
) -> io::Result<usize> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::FileExt;
        file.read_at(buffer, offset)
    }
    #[cfg(windows)]
    {
        use std::os::windows::fs::FileExt;
        file.seek_read(buffer, offset)
    }
    #[cfg(not(any(unix, windows)))]
    {
        use std::io::{Read, Seek, SeekFrom};
        let _guard = POSITIONED_READ_LOCK
            .lock()
            .unwrap_or_else(|error| error.into_inner());
        let mut file = file;
        file.seek(SeekFrom::Start(offset))?;
        file.read(buffer)
    }
}

#[cfg(any(test, not(any(unix, windows))))]
static POSITIONED_READ_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

fn read_native_exact_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::FileExt;
        read_exact_with(buffer, offset, |buffer, offset| {
            file.read_at(buffer, offset)
        })
    }
    #[cfg(windows)]
    {
        use std::os::windows::fs::FileExt;
        read_exact_with(buffer, offset, |buffer, offset| {
            file.seek_read(buffer, offset)
        })
    }
    #[cfg(not(any(unix, windows)))]
    {
        read_exact_at_fallback(file, buffer, offset)
    }
}

fn read_exact_with(
    mut buffer: &mut [u8],
    mut offset: u64,
    mut read_at: impl FnMut(&mut [u8], u64) -> io::Result<usize>,
) -> io::Result<()> {
    let length = u64::try_from(buffer.len()).map_err(|_| invalid_range())?;
    offset.checked_add(length).ok_or_else(invalid_range)?;
    while !buffer.is_empty() {
        match read_at(buffer, offset) {
            Ok(0) => {
                return Err(io::Error::new(
                    ErrorKind::UnexpectedEof,
                    "failed to fill whole buffer",
                ));
            }
            Ok(read) => {
                // The complete range was checked before I/O. File reads cannot
                // return more bytes than the remaining buffer can hold.
                offset += read as u64;
                buffer = &mut buffer[read..];
            }
            Err(error) if error.kind() == ErrorKind::Interrupted => {}
            Err(error) => return Err(error),
        }
    }
    Ok(())
}

fn invalid_range() -> io::Error {
    io::Error::new(ErrorKind::InvalidInput, "read range end overflows u64")
}

#[cfg(any(test, not(any(unix, windows))))]
fn read_exact_at_fallback(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<()> {
    use std::io::{Read, Seek, SeekFrom};
    // Cloning a File can share its cursor. Serialize the whole sequence across
    // callers of this helper when the platform has no positioned-read API.
    let _guard = POSITIONED_READ_LOCK
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    let mut file = file;
    read_exact_with(buffer, offset, |buffer, offset| {
        file.seek(SeekFrom::Start(offset))?;
        file.read(buffer)
    })
}

#[cfg(test)]
#[path = "io/tests.rs"]
mod tests;