Skip to content

Commit 0dae71f

Browse files
committed
fix: Don't panic when reading empty files
1 parent 6a50c12 commit 0dae71f

File tree

2 files changed

+83
-39
lines changed

2 files changed

+83
-39
lines changed

crates/core/src/vfs.rs

Lines changed: 77 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -455,17 +455,8 @@ impl Vfs {
455455
#[derive(Debug)]
456456
pub struct OpenFile {
457457
// The list of blobs
458-
content: Vec<BlobInfo>,
459-
}
460-
461-
// Information about the blob: 1) The id 2) The cumulated sizes of all blobs prior to this one, a.k.a the starting point of this blob.
462-
#[derive(Debug)]
463-
struct BlobInfo {
464-
// [`Id`] of the blob
465-
id: DataId,
466-
467-
// the start position of this blob within the file
468-
starts_at: usize,
458+
content: Vec<DataId>,
459+
offsets: ContentOffset,
469460
}
470461

471462
impl OpenFile {
@@ -488,26 +479,15 @@ impl OpenFile {
488479
///
489480
/// * Panics if the `Node` has no content
490481
pub fn from_node<P, S: IndexedFull>(repo: &Repository<P, S>, node: &Node) -> Self {
491-
let mut start = 0;
492-
let mut content: Vec<_> = node
493-
.content
494-
.as_ref()
495-
.unwrap()
496-
.iter()
497-
.map(|id| {
498-
let starts_at = start;
499-
start += repo.index().get_data(id).unwrap().data_length() as usize;
500-
BlobInfo { id: *id, starts_at }
501-
})
502-
.collect();
482+
let content: Vec<_> = node.content.as_ref().unwrap().clone();
503483

504-
// content is assumed to be partitioned, so we add a starts_at:MAX entry
505-
content.push(BlobInfo {
506-
id: DataId::default(),
507-
starts_at: usize::MAX,
508-
});
484+
let offsets = ContentOffset::from_sizes(
485+
content
486+
.iter()
487+
.map(|id| repo.index().get_data(id).unwrap().data_length() as usize),
488+
);
509489

510-
Self { content }
490+
Self { content, offsets }
511491
}
512492

513493
/// Read the `OpenFile` at the given `offset` from the `repo`.
@@ -530,19 +510,16 @@ impl OpenFile {
530510
pub fn read_at<P, S: IndexedFull>(
531511
&self,
532512
repo: &Repository<P, S>,
533-
mut offset: usize,
513+
offset: usize,
534514
mut length: usize,
535515
) -> RusticResult<Bytes> {
536-
// find the start of relevant blobs => find the largest index such that self.content[i].starts_at <= offset, but
537-
// self.content[i+1] > offset (note that a last dummy element has been added)
538-
let mut i = self.content.partition_point(|c| c.starts_at <= offset) - 1;
539-
540-
offset -= self.content[i].starts_at;
516+
let (mut i, mut offset) = self.offsets.offset(offset);
541517

542518
let mut result = BytesMut::with_capacity(length);
543519

544-
while length > 0 && i < self.content.len() - 1 {
545-
let data = repo.get_blob_cached(&BlobId::from(*self.content[i].id), BlobType::Data)?;
520+
// The case of empty node.content is also correctly handled here
521+
while length > 0 && i < self.content.len() {
522+
let data = repo.get_blob_cached(&BlobId::from(self.content[i]), BlobType::Data)?;
546523

547524
if offset > data.len() {
548525
// we cannot read behind the blob. This only happens if offset is too large to fit in the last blob
@@ -563,3 +540,66 @@ impl OpenFile {
563540
Ok(result.into())
564541
}
565542
}
543+
544+
#[derive(Debug)]
545+
struct ContentOffset(Vec<usize>);
546+
547+
impl ContentOffset {
548+
fn from_sizes(sizes: impl IntoIterator<Item = usize>) -> Self {
549+
let mut start = 0;
550+
let mut offsets: Vec<_> = sizes
551+
.into_iter()
552+
.map(|size| {
553+
let starts_at = start;
554+
start += size;
555+
starts_at
556+
})
557+
.collect();
558+
559+
if !offsets.is_empty() {
560+
// offsets is assumed to be partitioned, so we add a starts_at:MAX entry
561+
offsets.push(usize::MAX);
562+
}
563+
Self(offsets)
564+
}
565+
566+
fn offset(&self, mut offset: usize) -> (usize, usize) {
567+
if self.0.is_empty() {
568+
return (0, 0);
569+
}
570+
// find the start of relevant blobs => find the largest index such that self.offsets[i] <= offset, but
571+
// self.offsets[i+1] > offset (note that a last dummy element with usize::MAX has been added to ensure we always have two partitions)
572+
// If offsets is non-empty, then offsets[0] = 0, hence partition_point returns an index >=1.
573+
let i = self.0.partition_point(|o| o <= &offset) - 1;
574+
offset -= self.0[i];
575+
(i, offset)
576+
}
577+
}
578+
579+
#[cfg(test)]
580+
mod tests {
581+
use super::*;
582+
583+
#[test]
584+
fn content_offsets_empty_sizes() {
585+
let offsets = ContentOffset::from_sizes([]);
586+
assert_eq!(offsets.offset(0), (0, 0));
587+
assert_eq!(offsets.offset(42), (0, 0));
588+
}
589+
590+
#[test]
591+
fn content_offsets_size() {
592+
let offsets = ContentOffset::from_sizes([15]);
593+
assert_eq!(offsets.offset(0), (0, 0));
594+
assert_eq!(offsets.offset(5), (0, 5));
595+
assert_eq!(offsets.offset(20), (0, 20));
596+
}
597+
#[test]
598+
fn content_offsets_sizes() {
599+
let offsets = ContentOffset::from_sizes([15, 24]);
600+
assert_eq!(offsets.offset(0), (0, 0));
601+
assert_eq!(offsets.offset(5), (0, 5));
602+
assert_eq!(offsets.offset(20), (1, 5));
603+
assert_eq!(offsets.offset(42), (1, 27));
604+
}
605+
}

crates/core/tests/integration.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,15 @@ fn test_ls_and_read(
456456

457457
let data = repo.read_file_at(&file, 0, 21)?; // read full content
458458
assert_eq!(Bytes::from("This is a test file.\n"), &data);
459-
let data2 = repo.read_file_at(&file, 0, 4096)?; // read beyond file end
460-
assert_eq!(data2, &data);
459+
assert_eq!(data, repo.read_file_at(&file, 0, 4096)?); // read beyond file end
461460
assert_eq!(Bytes::new(), repo.read_file_at(&file, 25, 1)?); // offset beyond file end
462461
assert_eq!(Bytes::from("test"), repo.read_file_at(&file, 10, 4)?); // read partial content
463462

463+
// test reading an empty file from the repository
464+
let path: PathBuf = ["test", "0", "tests", "empty-file"].iter().collect();
465+
let node = entries.get(&path).unwrap();
466+
let file = repo.open_file(node)?;
467+
assert_eq!(Bytes::new(), repo.read_file_at(&file, 0, 0)?); // empty files
464468
Ok(())
465469
}
466470

0 commit comments

Comments
 (0)