Montagem de discos virtuais: desbloqueio de LUKS, BitLocker e FileVault 2 integrado à varredura e à recuperação
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a2ce82315e
commit
c014526761
9 files changed
+457
-24
No files matched your search
@@ -0,0 +1,184 @@
|
||||
//! Monta o disco virtual a partir do que o usuário informou: membros extras (RAID/LVM) e
|
||||
//! senhas ou chaves para volumes cifrados. É a única porta de entrada para varrer e recuperar
|
||||
//! discos que não são "simples".
|
||||
|
||||
use crate::crypt::{self, Secret, UnlockError};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::partition;
|
||||
use crate::source::{open_path, Source, SubSource};
|
||||
use crate::types::{FsKind, PartInfo};
|
||||
use crate::volume::{Overlay, Region};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum SecretInput {
|
||||
Password { value: String },
|
||||
Recovery { value: String },
|
||||
/// Caminho de um arquivo de chave (.BEK do BitLocker, keyfile do LUKS).
|
||||
Keyfile { path: String },
|
||||
/// Chave do volume em hexadecimal.
|
||||
Key { hex: String },
|
||||
}
|
||||
|
||||
/// Chave de volume guardada na sessão (só quando o usuário pede para lembrar).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SavedKey {
|
||||
pub start: u64,
|
||||
pub kind: String,
|
||||
pub key_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Assembly {
|
||||
/// Outros discos ou imagens do mesmo conjunto (RAID/LVM), além do alvo principal.
|
||||
#[serde(default)]
|
||||
pub members: Vec<String>,
|
||||
/// Senhas e chaves: nunca vão para o disco junto com a sessão.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub secrets: Vec<SecretInput>,
|
||||
/// Guarda as chaves de volume derivadas na sessão, para reabrir sem pedir a senha de novo.
|
||||
#[serde(default)]
|
||||
pub remember_keys: bool,
|
||||
#[serde(default)]
|
||||
pub keys: Vec<SavedKey>,
|
||||
}
|
||||
|
||||
impl Assembly {
|
||||
pub fn is_plain(&self) -> bool {
|
||||
self.members.is_empty() && self.secrets.is_empty() && self.keys.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct UnlockInfo {
|
||||
pub start: u64,
|
||||
pub len: u64,
|
||||
pub kind: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LockedInfo {
|
||||
pub start: u64,
|
||||
pub kind: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
pub struct Assembled {
|
||||
pub source: Arc<dyn Source>,
|
||||
pub unlocked: Vec<UnlockInfo>,
|
||||
pub locked: Vec<LockedInfo>,
|
||||
/// Chaves derivadas, para guardar na sessão se o usuário permitir.
|
||||
pub keys: Vec<SavedKey>,
|
||||
/// Volumes lógicos / partições extras que não estão na tabela do disco.
|
||||
pub extra_parts: Vec<PartInfo>,
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
fn to_secrets(a: &Assembly) -> Result<Vec<Secret>> {
|
||||
let mut v = vec![];
|
||||
for s in &a.secrets {
|
||||
v.push(match s {
|
||||
SecretInput::Password { value } => Secret::Password(value.clone()),
|
||||
SecretInput::Recovery { value } => Secret::RecoveryKey(value.clone()),
|
||||
SecretInput::Keyfile { path } => Secret::KeyFile(
|
||||
std::fs::read(path).map_err(|e| Error::msg(format!("não foi possível ler o arquivo de chave {path}: {e}")))?,
|
||||
),
|
||||
SecretInput::Key { hex } => Secret::VolumeKey(
|
||||
crypt::from_hex(hex).ok_or_else(|| Error::msg("chave do volume em hexadecimal inválida"))?,
|
||||
),
|
||||
});
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn kind_name(fs: FsKind) -> &'static str {
|
||||
match fs {
|
||||
FsKind::Bitlocker => "bitlocker",
|
||||
FsKind::Luks => "luks",
|
||||
FsKind::Corestorage => "filevault",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
|
||||
fn try_unlock(window: Arc<dyn Source>, fs: FsKind, secrets: &[Secret]) -> crypt::UnlockResult {
|
||||
match fs {
|
||||
FsKind::Bitlocker => crypt::bitlocker::unlock(window, secrets),
|
||||
FsKind::Luks => crypt::luks::unlock(window, secrets),
|
||||
FsKind::Corestorage => crypt::filevault::unlock(window, secrets),
|
||||
_ => Err(UnlockError::Unsupported("tipo".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Abre `path` e aplica a montagem pedida.
|
||||
pub fn assemble(path: &str, asm: &Assembly) -> Result<Assembled> {
|
||||
let base = open_path(Path::new(path)).map_err(|e| Error::msg(format!("{path}: {e}")))?;
|
||||
assemble_source(base, asm)
|
||||
}
|
||||
|
||||
pub fn assemble_source(base: Arc<dyn Source>, asm: &Assembly) -> Result<Assembled> {
|
||||
let secrets = to_secrets(asm)?;
|
||||
let mut regions: Vec<Region> = vec![];
|
||||
let mut out = Assembled {
|
||||
source: base.clone(),
|
||||
unlocked: vec![],
|
||||
locked: vec![],
|
||||
keys: vec![],
|
||||
extra_parts: vec![],
|
||||
notes: vec![],
|
||||
};
|
||||
// Até três rodadas: o que foi aberto pode conter outro volume cifrado.
|
||||
for _ in 0..3 {
|
||||
let current: Arc<dyn Source> = Arc::new(Overlay::new(base.clone(), regions.clone()));
|
||||
let layout = partition::analyze(current.as_ref(), false);
|
||||
let mut progress = false;
|
||||
out.locked.clear();
|
||||
for p in &layout.partitions {
|
||||
if p.encrypted.is_none() || regions.iter().any(|r| r.start == p.start) {
|
||||
continue;
|
||||
}
|
||||
let len = if p.len == 0 { current.size().saturating_sub(p.start) } else { p.len };
|
||||
let window: Arc<dyn Source> = Arc::new(SubSource::new(current.clone(), p.start, len));
|
||||
let mut all = secrets.clone();
|
||||
for k in asm.keys.iter().filter(|k| k.start == p.start) {
|
||||
if let Some(b) = crypt::from_hex(&k.key_hex) {
|
||||
all.push(Secret::VolumeKey(b));
|
||||
}
|
||||
}
|
||||
match try_unlock(window, p.fs, &all) {
|
||||
Ok(u) => {
|
||||
let ulen = u.source.size();
|
||||
out.unlocked.push(UnlockInfo {
|
||||
start: p.start,
|
||||
len: ulen,
|
||||
kind: kind_name(p.fs).into(),
|
||||
description: u.description.clone(),
|
||||
});
|
||||
if let Some(k) = &u.volume_key {
|
||||
out.keys.push(SavedKey { start: p.start, kind: kind_name(p.fs).into(), key_hex: crypt::to_hex(k) });
|
||||
}
|
||||
regions.push(Region { start: p.start, len: ulen, src: u.source, label: u.description });
|
||||
progress = true;
|
||||
}
|
||||
Err(e) => out.locked.push(LockedInfo {
|
||||
start: p.start,
|
||||
kind: kind_name(p.fs).into(),
|
||||
reason: match e {
|
||||
UnlockError::WrongSecret if secrets.is_empty() && asm.keys.is_empty() => "volume cifrado: informe a senha, a chave de recuperação ou o arquivo de chave".into(),
|
||||
other => other.to_string(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
if !progress {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !regions.is_empty() {
|
||||
out.source = Arc::new(Overlay::new(base, regions));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -95,7 +95,6 @@ struct Ext<'a> {
|
||||
inodes: u64,
|
||||
groups: Vec<Group>,
|
||||
journal_inum: u32,
|
||||
incompat: u32,
|
||||
}
|
||||
|
||||
/// Sequência de (bloco lógico, bloco físico, quantidade, não-inicializado).
|
||||
@@ -163,7 +162,6 @@ impl<'a> Ext<'a> {
|
||||
inodes: le32(&sb, 0) as u64,
|
||||
groups,
|
||||
journal_inum: le32(&sb, 224),
|
||||
incompat,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -327,19 +325,6 @@ impl<'a> Ext<'a> {
|
||||
out
|
||||
}
|
||||
|
||||
fn read_file(&self, ino: &Inode, max: u64) -> Vec<u8> {
|
||||
let size = ino.size.min(max);
|
||||
let mut out = Vec::with_capacity(size as usize);
|
||||
for e in self.file_extents(ino, size) {
|
||||
if e.sparse {
|
||||
out.extend(std::iter::repeat(0).take(e.len as usize));
|
||||
} else if let Ok(v) = read_vec(self.src, e.offset, e.len as usize) {
|
||||
out.extend(v);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Mapa de blocos livres (bitmap), em extents absolutos.
|
||||
fn free_map(&self, ctx: &Ctx) -> (Vec<Extent>, Vec<bool>) {
|
||||
let mut free: Vec<Extent> = vec![];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Princípio central: a origem é aberta **somente para leitura**; nada neste
|
||||
//! crate escreve no dispositivo que está sendo recuperado.
|
||||
|
||||
pub mod assemble;
|
||||
pub mod carve;
|
||||
pub mod device;
|
||||
pub mod crypt;
|
||||
@@ -31,6 +32,7 @@ pub mod source;
|
||||
pub mod sqlite;
|
||||
pub mod thumb;
|
||||
pub mod types;
|
||||
pub mod volume;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use types::*;
|
||||
|
||||
@@ -13,7 +13,21 @@ pub fn detect_fs(src: &dyn Source, start: u64) -> Option<(FsKind, Option<u64>)>
|
||||
if b.len() < 1100 {
|
||||
return None;
|
||||
}
|
||||
detect_fs_in(&b)
|
||||
if let Some(r) = detect_fs_in(&b) {
|
||||
return Some(r);
|
||||
}
|
||||
// Assinaturas mais adiante no volume: Btrfs (64 KiB) e RAID mdadm 1.2 (4 KiB).
|
||||
if let Ok(h) = read_vec(src, start + 0x10000, 0x100) {
|
||||
if h.len() >= 0x80 && &h[0x40..0x48] == b"_BHRfS_M" {
|
||||
return Some((FsKind::Btrfs, Some(u64::from_le_bytes(h[0x70..0x78].try_into().unwrap()))));
|
||||
}
|
||||
}
|
||||
if let Ok(h) = read_vec(src, start + 4096, 16) {
|
||||
if h.len() >= 4 && h[..4] == [0xFC, 0x4E, 0x2B, 0xA9] {
|
||||
return Some((FsKind::Raid, None));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Mesma detecção sobre um buffer que começa no setor candidato (>= 1100 bytes).
|
||||
@@ -35,6 +49,27 @@ pub fn detect_fs_in(b: &[u8]) -> Option<(FsKind, Option<u64>)> {
|
||||
return Some((FsKind::Exfat, Some(vol << shift)));
|
||||
}
|
||||
}
|
||||
if b.len() >= 520 && &b[512..520] == b"LABELONE" {
|
||||
return Some((FsKind::Lvm, None));
|
||||
}
|
||||
if b[..4] == [0xFC, 0x4E, 0x2B, 0xA9] {
|
||||
return Some((FsKind::Raid, None));
|
||||
}
|
||||
if &b[0..4] == b"XFSB" {
|
||||
let bs = u32::from_be_bytes(b[4..8].try_into().unwrap()) as u64;
|
||||
let blocks = u64::from_be_bytes(b[8..16].try_into().unwrap());
|
||||
if bs.is_power_of_two() && (512..=65536).contains(&bs) {
|
||||
return Some((FsKind::Xfs, Some(bs * blocks)));
|
||||
}
|
||||
}
|
||||
if &b[3..7] == b"ReFS" && &b[16..20] == b"FSRS" {
|
||||
let sectors = u64::from_le_bytes(b[24..32].try_into().unwrap());
|
||||
let bps = u32::from_le_bytes(b[32..36].try_into().unwrap()) as u64;
|
||||
return Some((FsKind::Refs, Some(sectors * bps)));
|
||||
}
|
||||
if crate::crypt::filevault::detect(b) {
|
||||
return Some((FsKind::Corestorage, Some(u64::from_le_bytes(b[64..72].try_into().unwrap()))));
|
||||
}
|
||||
if crate::crypt::bitlocker::detect(b) {
|
||||
return Some((FsKind::Bitlocker, None));
|
||||
}
|
||||
|
||||
@@ -83,6 +83,15 @@ pub fn run(
|
||||
l
|
||||
}
|
||||
};
|
||||
let mut layout = layout;
|
||||
// Volumes lógicos e outros volumes montados que não estão na tabela do disco.
|
||||
for p in &opts.extra_parts {
|
||||
if !layout.partitions.iter().any(|q| q.start == p.start) {
|
||||
let mut p = p.clone();
|
||||
p.index = layout.partitions.len();
|
||||
layout.partitions.push(p);
|
||||
}
|
||||
}
|
||||
cp.layout = Some(layout.clone());
|
||||
|
||||
// Índices para fusão: primeiro offset -> (id, confiança), e extents de achados por metadados.
|
||||
|
||||
@@ -204,10 +204,25 @@ impl Engine {
|
||||
|
||||
// ---------------------------------------------------------------- varredura
|
||||
|
||||
pub fn start_scan(&self, path: &str, opts: ScanOptions) -> Result<String> {
|
||||
let src = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||
let fp = crate::source::fingerprint(src.as_ref());
|
||||
let job = self.register(JobKind::Scan, path.to_string(), opts, Some(src));
|
||||
pub fn start_scan(&self, path: &str, mut opts: ScanOptions) -> Result<String> {
|
||||
let raw = open_path(Path::new(path)).map_err(|e| open_error(path, e))?;
|
||||
let fp = crate::source::fingerprint(raw.as_ref());
|
||||
let asm = crate::assemble::assemble_source(raw, &opts.assembly)?;
|
||||
opts.extra_parts = asm.extra_parts.clone();
|
||||
if opts.assembly.remember_keys {
|
||||
opts.assembly.keys = asm.keys.clone();
|
||||
}
|
||||
let job = self.register(JobKind::Scan, path.to_string(), opts, Some(asm.source.clone()));
|
||||
{
|
||||
let mut s = job.st.lock().unwrap();
|
||||
for u in &asm.unlocked {
|
||||
s.warnings.push(format!("Volume cifrado aberto na posição {}: {}.", u.start, u.description));
|
||||
}
|
||||
for l in &asm.locked {
|
||||
s.warnings.push(format!("Volume cifrado na posição {} não foi aberto: {}.", l.start, l.reason));
|
||||
}
|
||||
s.warnings.extend(asm.notes.iter().cloned());
|
||||
}
|
||||
*job.fingerprint.lock().unwrap() = fp;
|
||||
*job.session_file.lock().unwrap() = Some(new_session_path(path));
|
||||
spawn_scan(job.clone(), None);
|
||||
@@ -814,8 +829,8 @@ impl Engine {
|
||||
let mut last_err = None;
|
||||
for path in &tried {
|
||||
match open_path(Path::new(path)) {
|
||||
Ok(src) if crate::reconnect::matches(src.as_ref(), size, fp.as_deref()) => {
|
||||
*j.src.lock().unwrap() = Some(src);
|
||||
Ok(src) if crate::reconnect::matches(src.as_ref(), if j.opts.assembly.is_plain() { size } else { 0 }, fp.as_deref()) => {
|
||||
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||
*j.src_path.lock().unwrap() = path.clone();
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -826,7 +841,7 @@ impl Engine {
|
||||
if let Some((path, src)) =
|
||||
crate::reconnect::find_matching_source(size, fp.as_deref(), &tried)
|
||||
{
|
||||
*j.src.lock().unwrap() = Some(src);
|
||||
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||
*j.src_path.lock().unwrap() = path.clone();
|
||||
return Ok(Some(path));
|
||||
}
|
||||
@@ -863,7 +878,7 @@ impl Engine {
|
||||
if fp.is_none() {
|
||||
*j.fingerprint.lock().unwrap() = crate::source::fingerprint(src.as_ref());
|
||||
}
|
||||
*j.src.lock().unwrap() = Some(src);
|
||||
*j.src.lock().unwrap() = Some(finish_source(&j, src));
|
||||
*j.src_path.lock().unwrap() = path.to_string();
|
||||
j.st.lock()
|
||||
.unwrap()
|
||||
@@ -1483,6 +1498,14 @@ impl Engine {
|
||||
|
||||
// ---------------------------------------------------------------- sessões automáticas
|
||||
|
||||
/// Reaplica ao disco reaberto a montagem (volumes desbloqueados, RAID) da sessão.
|
||||
fn finish_source(j: &Job, raw: Arc<dyn Source>) -> Arc<dyn Source> {
|
||||
match crate::assemble::assemble_source(raw.clone(), &j.opts.assembly) {
|
||||
Ok(a) => a.source,
|
||||
Err(_) => raw,
|
||||
}
|
||||
}
|
||||
|
||||
fn sessions_dir() -> PathBuf {
|
||||
crate::settings::config_dir().join("sessoes")
|
||||
}
|
||||
|
||||
@@ -210,8 +210,17 @@ pub enum FsKind {
|
||||
Ext,
|
||||
Apfs,
|
||||
HfsPlus,
|
||||
Btrfs,
|
||||
Xfs,
|
||||
Refs,
|
||||
Bitlocker,
|
||||
Luks,
|
||||
/// FileVault 2 (Core Storage).
|
||||
Corestorage,
|
||||
/// Membro de volume LVM (physical volume).
|
||||
Lvm,
|
||||
/// Membro de RAID por software (mdadm).
|
||||
Raid,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@@ -226,8 +235,14 @@ impl FsKind {
|
||||
FsKind::Ext => "ext2/3/4",
|
||||
FsKind::Apfs => "APFS",
|
||||
FsKind::HfsPlus => "HFS+",
|
||||
FsKind::Btrfs => "Btrfs",
|
||||
FsKind::Xfs => "XFS",
|
||||
FsKind::Refs => "ReFS",
|
||||
FsKind::Bitlocker => "BitLocker",
|
||||
FsKind::Luks => "LUKS",
|
||||
FsKind::Corestorage => "FileVault 2",
|
||||
FsKind::Lvm => "LVM",
|
||||
FsKind::Raid => "RAID (mdadm)",
|
||||
FsKind::Unknown => "desconhecido",
|
||||
}
|
||||
}
|
||||
@@ -237,6 +252,7 @@ impl FsKind {
|
||||
match self {
|
||||
FsKind::Bitlocker => Some("BitLocker"),
|
||||
FsKind::Luks => Some("LUKS"),
|
||||
FsKind::Corestorage => Some("FileVault 2"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -302,6 +318,12 @@ pub struct ScanOptions {
|
||||
/// Tamanho máximo aceito para arquivos carved (bytes).
|
||||
#[serde(default = "default_max_carve")]
|
||||
pub max_carve_size: u64,
|
||||
/// Membros extras (RAID/LVM) e segredos para volumes cifrados.
|
||||
#[serde(default)]
|
||||
pub assembly: crate::assemble::Assembly,
|
||||
/// Partições que não estão na tabela do disco (volumes lógicos, desbloqueados).
|
||||
#[serde(default)]
|
||||
pub extra_parts: Vec<PartInfo>,
|
||||
}
|
||||
|
||||
fn default_max_carve() -> u64 {
|
||||
@@ -315,6 +337,8 @@ impl Default for ScanOptions {
|
||||
include_allocated: false,
|
||||
categories: vec![],
|
||||
max_carve_size: default_max_carve(),
|
||||
assembly: Default::default(),
|
||||
extra_parts: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Disco virtual: a origem real com trechos substituídos por volumes desbloqueados,
|
||||
//! membros de RAID montados ou volumes lógicos (LVM) anexados depois do fim do disco.
|
||||
//!
|
||||
//! Os deslocamentos continuam sendo "do disco": quem varre e quem recupera enxergam o mesmo
|
||||
//! espaço de endereços, desde que a montagem seja refeita igual (a `Assembly` é determinística).
|
||||
|
||||
use crate::source::Source;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Region {
|
||||
pub start: u64,
|
||||
pub len: u64,
|
||||
pub src: Arc<dyn Source>,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
pub struct Overlay {
|
||||
base: Arc<dyn Source>,
|
||||
regions: Vec<Region>,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl Overlay {
|
||||
pub fn new(base: Arc<dyn Source>, mut regions: Vec<Region>) -> Self {
|
||||
regions.sort_by_key(|r| r.start);
|
||||
let size = regions.iter().map(|r| r.start + r.len).max().unwrap_or(0).max(base.size());
|
||||
Self { base, regions, size }
|
||||
}
|
||||
pub fn regions(&self) -> &[Region] {
|
||||
&self.regions
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for Overlay {
|
||||
fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if offset >= self.size || buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let want = buf.len().min((self.size - offset) as usize);
|
||||
let i = self.regions.partition_point(|r| r.start + r.len <= offset);
|
||||
if let Some(r) = self.regions.get(i) {
|
||||
if r.start <= offset {
|
||||
let n = want.min((r.start + r.len - offset) as usize);
|
||||
return r.src.read_at(offset - r.start, &mut buf[..n]);
|
||||
}
|
||||
// Antes da próxima região: lê da base só até ela.
|
||||
let n = want.min((r.start - offset) as usize);
|
||||
return self.read_base(offset, &mut buf[..n]);
|
||||
}
|
||||
self.read_base(offset, &mut buf[..want])
|
||||
}
|
||||
}
|
||||
|
||||
impl Overlay {
|
||||
fn read_base(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if offset >= self.base.size() {
|
||||
// Lacuna entre a base e uma região anexada.
|
||||
buf.fill(0);
|
||||
return Ok(buf.len());
|
||||
}
|
||||
let n = buf.len().min((self.base.size() - offset) as usize);
|
||||
let got = self.base.read_at(offset, &mut buf[..n])?;
|
||||
if got < buf.len() && got == n {
|
||||
buf[n..].fill(0);
|
||||
return Ok(buf.len());
|
||||
}
|
||||
Ok(got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user