Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  lib.rs

  Sprache: Rust
 

/* 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 http://mozilla.org/MPL/2.0/. */


use adler2::adler32_slice;
use log::warn;
use nsstring::nsCString;
use serde::{Deserialize, Serialize};
use static_assertions::const_assert;
use std::ffi::c_void;
use std::io::Write as _;
use std::path::Path;
use thin_vec::ThinVec;

/// Callback type for [`ssl_tokens_cache_read`].
pub type SslTokensReadCallback =
    unsafe extern "C" fn(ctx: *mut c_void, record: *const SslTokensPersistedRecord);

/// FFI-safe representation of one persisted token record.
#[repr(C)]
pub struct SslTokensPersistedRecord {
    pub id: u64,
    pub key: nsCString,
    pub expiration_time: PrTime,
    /// Cached for `ShouldPersistKey` filtering without decompressing the payload.
    pub overridable_error: u8,
    pub compressed_payload: *const u8,
    pub compressed_payload_len: usize,
}

#[derive(Clone, Serialize, Deserialize)]
#[expect(
    clippy::unsafe_derive_deserialize,
    reason = "from_record is unrelated to deserialization"
)]
struct PersistedRecord {
    id: u64,
    key: Vec<u8>,
    expiration_time: PrTime,
    overridable_error: u8,
    compressed_payload: Vec<u8>,
}

impl PersistedRecord {
    /// # Safety
    ///
    /// `compressed_payload` must be valid for `compressed_payload_len` bytes.
    unsafe fn from_record(rec: &SslTokensPersistedRecord) -> Self {
        let key = rec.key.as_ref().to_vec();
        // SAFETY: compressed_payload is valid for compressed_payload_len bytes.
        let compressed_payload = unsafe {
            std::slice::from_raw_parts(rec.compressed_payload, rec.compressed_payload_len)
        }
        .to_vec();
        Self {
            id: rec.id,
            key,
            expiration_time: rec.expiration_time,
            overridable_error: rec.overridable_error,
            compressed_payload,
        }
    }

    fn with_record<F: FnOnce(&SslTokensPersistedRecord)>(&self, f: F) {
        let rec = SslTokensPersistedRecord {
            id: self.id,
            key: nsCString::from(self.key.as_slice()),
            expiration_time: self.expiration_time,
            overridable_error: self.overridable_error,
            compressed_payload: self.compressed_payload.as_ptr(),
            compressed_payload_len: self.compressed_payload.len(),
        };
        f(&rec);
    }
}

/// Microseconds since the Unix epoch, matching the C++ `PRTime` type.
type PrTime = i64;

const MAGIC: [u8; 4] = *b"STCF";
const VERSION: u8 = 3;
/// File layout: magic(4) + version(1) + `bincode_body(N)` + `adler32_le(4)`.
/// VERSION 3: each record stores a single compressed payload (token + cert
/// info together) instead of separate fields.
const HEADER_SIZE: usize = MAGIC.len() + size_of::<u8>();
const_assert!(HEADER_SIZE == 5);
/// Sanity cap on the bincode body to guard against corrupt size fields.
const MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024;

#[derive(Debug)]
enum ParseError {
    BadMagic,
    BadVersion,
    Truncated,
}

fn to_file_bytes(records: &[PersistedRecord], magic: [u8; 4]) -> Vec<u8> {
    let body = bincode::serialize(records).unwrap_or_default();
    let checksum = adler32_slice(&body).to_le_bytes();
    let mut out = Vec::with_capacity(HEADER_SIZE + body.len() + 4);
    out.extend_from_slice(&magic);
    out.push(VERSION);
    out.extend_from_slice(&body);
    out.extend_from_slice(&checksum);
    out
}

fn from_file_bytes(
    data: &[u8],
    expected_magic: [u8; 4],
) -> Result<Vec<PersistedRecord>, ParseError> {
    let Some(([magic @ .., version], rest)) = data.split_first_chunk::<HEADER_SIZE>() else {
        return Err(ParseError::Truncated);
    };
    if magic != &expected_magic {
        return Err(ParseError::BadMagic);
    }
    if *version != VERSION {
        return Err(ParseError::BadVersion);
    }
    let Some((body, stored)) = rest.split_last_chunk::<4>() else {
        return Err(ParseError::Truncated);
    };
    if body.len() > MAX_PAYLOAD_SIZE {
        return Err(ParseError::Truncated);
    }
    if adler32_slice(body).to_le_bytes() != *stored {
        return Err(ParseError::Truncated);
    }
    bincode::deserialize::<Vec<PersistedRecord>>(body).map_err(|_| ParseError::Truncated)
}

/// Reads `bin_path`, falling back to `bin_path.with_extension("tmp")` if the
/// canonical file is absent (crash-mid-rename recovery). Discards a stale .tmp
/// when the .bin is present. Returns `(data, loaded_from_tmp)` or `None`.
fn read_file_with_tmp_fallback(bin_path: &Path) -> Option<(Vec<u8>, bool)> {
    let tmp_path = bin_path.with_extension("tmp");
    std::fs::read(bin_path)
        .map(|data| {
            _ = std::fs::remove_file(&tmp_path);
            (data, false)
        })
        .or_else(|_| std::fs::read(&tmp_path).map(|data| (data, true)))
        .ok()
}

fn nscstring_as_path(s: &nsCString) -> Option<&Path> {
    std::str::from_utf8(s.as_ref()).ok().map(Path::new)
}

fn write_atomically(buf: &[u8], bin_path: &Path) -> std::io::Result<()> {
    let tmp_path = bin_path.with_extension("tmp");
    let mut f = std::fs::File::create(&tmp_path)?; // nosemgrep
    f.write_all(buf)?;
    f.sync_all()?;
    std::fs::rename(tmp_path, bin_path)
}

/// Calls `callback` for each non-expired record, passing a stack-allocated FFI
/// struct. The pointer passed to the callback is only valid during the call.
///
/// # Safety
///
/// `callback` must be a valid function pointer. `ctx` must remain valid for the
/// duration of this call.
unsafe fn dispatch_records(
    records: &[PersistedRecord],
    now: PrTime,
    callback: SslTokensReadCallback,
    ctx: *mut c_void,
) {
    for rec in records.iter().filter(|r| r.expiration_time > now) {
        // SAFETY: callback is a valid function pointer and ctx is caller-managed.
        rec.with_record(|c_rec| unsafe { callback(ctx, &raw const *c_rec) });
    }
}

/// Reads the persisted file and calls `callback` for each valid record.
///
/// # Safety
///
/// `callback` must be a valid function pointer. `ctx` must remain valid for
/// the duration of this call. The `callback` is invoked with a pointer to a
/// stack-allocated FFI struct; the pointer is only valid inside the callback.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ssl_tokens_cache_read(
    path: &nsCString,
    now: PrTime,
    callback: SslTokensReadCallback,
    ctx: *mut c_void,
) {
    let Some(bin_path) = nscstring_as_path(path) else {
        return;
    };

    let Some((data, loaded_from_tmp)) = read_file_with_tmp_fallback(bin_path) else {
        return;
    };

    let records = match from_file_bytes(&data, MAGIC) {
        Ok(r) => r,
        Err(e) => {
            let bad = if loaded_from_tmp {
                bin_path.with_extension("tmp")
            } else {
                bin_path.to_path_buf()
            };
            warn!(
                "SslTokensCache: parse error ({e:?}), discarding {}",
                bad.display()
            );
            _ = std::fs::remove_file(&bad);
            return;
        }
    };

    if loaded_from_tmp {
        _ = std::fs::rename(bin_path.with_extension("tmp"), bin_path);
    }

    // SAFETY: callback and ctx are valid for the duration of this call.
    unsafe {
        dispatch_records(&records, now, callback, ctx);
    }
}

/// Serializes `records` to STCF format, appending the bytes to `out`.
/// Stateless — does not touch any global state.
///
/// # Safety
///
/// Each record's `token` pointer must be valid and stable (no reallocation
/// of the owning buffer) for `token_len` bytes for the duration of this
/// call. `records` and `out` must be valid non-null references.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ssl_tokens_cache_serialize_snapshot(
    records: &ThinVec<SslTokensPersistedRecord>,
    out: &mut ThinVec<u8>,
) {
    let persisted: Vec<_> = records
        .iter()
        .map(|r| unsafe { PersistedRecord::from_record(r) })
        .collect();
    out.extend_from_slice(&to_file_bytes(&persisted, MAGIC));
}

/// Writes `data` atomically to `path` via the .tmp + rename dance.
///
/// # Safety
///
/// `path` and `data` must be valid non-null references.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ssl_tokens_cache_write_bytes(path: &nsCString, data: &ThinVec<u8>) {
    let Some(path) = nscstring_as_path(path) else {
        return;
    };
    if let Err(e) = write_atomically(data, path) {
        warn!("SslTokensCache: write failed: {e}");
    }
}

/// Parses an STCF-format buffer and dispatches each non-expired record to
/// `callback`.
///
/// # Safety
///
/// `data` must point to `data_len` valid bytes. `callback` must be a valid
/// function pointer. `ctx` must remain valid for the duration of this call.
/// The pointer passed to `callback` is stack-allocated and is only valid inside
/// the callback. `callback` is always invoked synchronously — it is never
/// called after this function returns, so callers may safely pass pointers to
/// stack-allocated context.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ssl_tokens_cache_deserialize_ipc(
    data: *const u8,
    data_len: usize,
    now: PrTime,
    callback: SslTokensReadCallback,
    ctx: *mut c_void,
) {
    // SAFETY: data points to data_len valid bytes per the contract.
    let bytes = unsafe { std::slice::from_raw_parts(data, data_len) };
    let records = match from_file_bytes(bytes, MAGIC) {
        Ok(r) => r,
        Err(e) => {
            warn!("SslTokensCache: IPC deserialize error ({e:?})");
            return;
        }
    };
    // SAFETY: callback and ctx are valid for the duration of this call.
    unsafe {
        dispatch_records(&records, now, callback, ctx);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    type TestResult = Result<(), Box<dyn std::error::Error>>;

    fn make_record(id: u64, key: &str, payload: &[u8], expiration_time: i64) -> PersistedRecord {
        PersistedRecord {
            id,
            key: key.as_bytes().to_vec(),
            expiration_time,
            overridable_error: 0,
            compressed_payload: payload.to_vec(),
        }
    }

    // --- to_file_bytes / from_file_bytes ---

    #[test]
    fn round_trip_empty() {
        let records = from_file_bytes(&to_file_bytes(&[], MAGIC), MAGIC).expect("valid file bytes");
        assert!(records.is_empty());
    }

    #[test]
    fn round_trip_records() {
        let input = vec![
            make_record(1"example.com:443", b"payload1", i64::MAX),
            make_record(2"other.net:443", b"payload2"9999),
        ];
        let output =
            from_file_bytes(&to_file_bytes(&input, MAGIC), MAGIC).expect("valid file bytes");
        assert_eq!(output.len(), 2);
        assert_eq!(output[0].key, b"example.com:443");
        assert_eq!(output[0].compressed_payload, b"payload1");
        assert_eq!(output[1].id, 2);
    }

    #[test]
    fn bad_magic() {
        let bytes = to_file_bytes(&[], MAGIC);
        assert!(matches!(
            from_file_bytes(&bytes, *b"XXXX"),
            Err(ParseError::BadMagic)
        ));
    }

    #[test]
    fn bad_version() {
        let mut bytes = to_file_bytes(&[], MAGIC);
        bytes[4] = VERSION.wrapping_add(1);
        assert!(matches!(
            from_file_bytes(&bytes, MAGIC),
            Err(ParseError::BadVersion)
        ));
    }

    #[test]
    fn corrupt_body() {
        // Corrupt a byte in the body — the Adler-32 checksum detects it.
        let mut bytes = to_file_bytes(&[], MAGIC);
        let body_start = HEADER_SIZE;
        bytes[body_start] ^= 0xFF;
        assert!(matches!(
            from_file_bytes(&bytes, MAGIC),
            Err(ParseError::Truncated)
        ));
    }

    #[test]
    fn truncated() {
        assert!(matches!(
            from_file_bytes(&[0u8; 4], MAGIC),
            Err(ParseError::Truncated)
        ));
    }

    // --- read_file_with_tmp_fallback ---

    #[test]
    fn fallback_bin_exists() -> TestResult {
        let dir = tempfile::tempdir()?;
        let bin = dir.path().join("cache.bin");
        let tmp_path = dir.path().join("cache.tmp");
        std::fs::write(&bin, b"bin")?;
        std::fs::write(&tmp_path, b"tmp")?;

        let (data, from_tmp) = read_file_with_tmp_fallback(&bin).expect("bin present");
        assert_eq!(data, b"bin");
        assert!(!from_tmp);
        assert!(!tmp_path.exists()); // stale .tmp deleted
        Ok(())
    }

    #[test]
    fn fallback_only_tmp_exists() -> TestResult {
        let dir = tempfile::tempdir()?;
        let bin = dir.path().join("cache.bin");
        std::fs::write(bin.with_extension("tmp"), b"recovered")?;

        let (data, from_tmp) = read_file_with_tmp_fallback(&bin).expect("tmp present");
        assert_eq!(data, b"recovered");
        assert!(from_tmp);
        Ok(())
    }

    #[test]
    fn fallback_neither_exists() -> TestResult {
        let dir = tempfile::tempdir()?;
        assert!(read_file_with_tmp_fallback(&dir.path().join("cache.bin")).is_none());
        Ok(())
    }

    // --- write_atomically ---

    #[test]
    fn write_atomically_leaves_no_tmp() -> TestResult {
        let dir = tempfile::tempdir()?;
        let bin = dir.path().join("cache.bin");
        write_atomically(b"hello", &bin)?;
        assert_eq!(std::fs::read(&bin)?, b"hello");
        assert!(!bin.with_extension("tmp").exists());
        Ok(())
    }

    #[test]
    fn write_atomically_round_trip_with_fallback() -> TestResult {
        let dir = tempfile::tempdir()?;
        let bin = dir.path().join("cache.bin");
        let records = vec![make_record(1"a.com:443", b"tok", i64::MAX)];
        write_atomically(&to_file_bytes(&records, MAGIC), &bin)?;
        let (data, from_tmp) = read_file_with_tmp_fallback(&bin).expect("bin present");
        assert!(!from_tmp);
        let out = from_file_bytes(&data, MAGIC).expect("valid file bytes");
        assert_eq!(out[0].key, b"a.com:443");
        Ok(())
    }

    #[test]
    fn ipc_deserialize_bad_data() {
        assert!(from_file_bytes(b"not valid STCF data at all", MAGIC).is_err());
    }
}

Messung V0.5 in Prozent
C=84 H=99 G=91

¤ Dauer der Verarbeitung: 0.5 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=752002