use std::collections::BTreeMap;
use shared_buffer::OwnedBuffer;
use super::v2;
use super::v3;
use crate::metadata::annotations::FileSystemMapping;
use crate::PathSegment;
use crate::ToPathSegments;
fn v2_to_v3_directory<'a>(dir: v2::write::Directory<'a>) -> v3::write::Directory<'a> {
let mut children = BTreeMap::new();
for (path, entry) in dir.children {
let entry = match entry {
v2::write::DirEntry::Dir(dir) => {
v3::write::volumes::DirEntry::Dir(v2_to_v3_directory(dir))
}
v2::write::DirEntry::File(file) => {
let file = match file {
v2::write::FileEntry::Borrowed(b) => {
v3::write::FileEntry::borrowed(b, v3::Timestamps::default())
}
v2::write::FileEntry::Owned(o) => {
v3::write::FileEntry::owned(o, v3::Timestamps::default())
}
v2::write::FileEntry::Reader(r) => {
v3::write::FileEntry::reader(r, v3::Timestamps::default())
}
};
v3::write::volumes::DirEntry::File(file)
}
};
children.insert(path, entry);
}
v3::write::Directory {
children,
timestamps: v3::Timestamps::default(),
}
}
fn v3_to_v2_directory<'a>(dir: v3::write::Directory<'a>) -> v2::write::Directory<'a> {
let mut children = BTreeMap::new();
for (path, entry) in dir.children {
let entry = match entry {
v3::write::DirEntry::Dir(dir) => v2::write::DirEntry::Dir(v3_to_v2_directory(dir)),
v3::write::DirEntry::File(file) => {
let file = match file.content {
v3::write::volumes::FileContent::Borrowed(b) => {
v2::write::FileEntry::Borrowed(b)
}
v3::write::volumes::FileContent::Owned(o) => v2::write::FileEntry::Owned(o),
v3::write::volumes::FileContent::Reader(r) => v2::write::FileEntry::Reader(r),
};
v2::write::DirEntry::File(file)
}
};
children.insert(path, entry);
}
v2::write::Directory { children }
}
pub fn v2_to_v3(webc: impl Into<OwnedBuffer>) -> Result<OwnedBuffer, anyhow::Error> {
let reader = v2::read::OwnedReader::parse(webc)?;
let mut manifest = reader.manifest().clone();
let mut fs_mappings = manifest.filesystem()?;
if let Some(fs_mappings) = fs_mappings.as_mut() {
for mapping in fs_mappings.0.iter_mut() {
mapping.volume_name = mapping.host_path.take().unwrap();
}
manifest.update_filesystem(fs_mappings.clone())?;
}
let atoms = reader
.iter_atoms()
.map(|(name, data)| {
let path = PathSegment::parse(name).unwrap();
let file_entry =
v3::write::FileEntry::owned(data.clone().into_bytes(), v3::Timestamps::default());
(path, file_entry)
})
.collect::<BTreeMap<PathSegment, v3::write::FileEntry<'_>>>();
let writer = v3::write::Writer::new(v3::ChecksumAlgorithm::Sha256);
let mut writer = writer.write_manifest(&manifest)?.write_atoms(atoms)?;
let mut volumes = BTreeMap::new();
for entry in reader.iter_volumes() {
let (name, section) = entry?;
let mut root: v2::write::Directory<'static> = section.root()?.try_into()?;
if name == "atom" {
if let Some(fs_mappings) = fs_mappings.as_ref() {
for FileSystemMapping { volume_name, .. } in fs_mappings.iter() {
let path_segments = volume_name.to_path_segments()?;
let mut curr_dir = &mut root;
for segment in path_segments {
let v2::write::DirEntry::Dir(dir) = curr_dir
.children
.get_mut(&segment)
.expect("{segment:?} is expected")
else {
panic!("{segment:?} must be a directory");
};
curr_dir = dir;
}
let curr_dir = std::mem::take(curr_dir);
let volume = v2_to_v3_directory(curr_dir);
volumes.insert(volume_name.clone(), volume);
}
}
} else if name == "metadata" {
let root = v2_to_v3_directory(root);
volumes.insert("metadata".to_string(), root);
} else {
panic!("Unknown volume {name:?}: webc v2 should only have a metadata volume and an atom volume")
}
}
for (name, volume) in volumes {
writer.write_volume(name.as_str(), volume)?;
}
writer
.finish(v3::SignatureAlgorithm::None)
.map(OwnedBuffer::from)
.map_err(anyhow::Error::from)
}
pub fn v3_to_v2(webc: impl Into<OwnedBuffer>) -> Result<OwnedBuffer, anyhow::Error> {
let reader = v3::read::OwnedReader::parse(webc)?;
let mut manifest = reader.manifest().clone();
let mut fs_mappings = manifest
.filesystem()?
.expect("File system mapping must be present on webc v2");
for mapping in fs_mappings.0.iter_mut() {
mapping.host_path = Some(mapping.volume_name.clone());
mapping.volume_name = "atom".to_string();
}
manifest.update_filesystem(fs_mappings)?;
let atoms = reader
.iter_atoms()
.map(|(name, _hash, data)| {
let path = PathSegment::parse(name).unwrap();
let file = v2::write::FileEntry::Owned(data.clone().into_bytes());
(path, file)
})
.collect::<BTreeMap<PathSegment, v2::write::FileEntry<'_>>>();
let writer = v2::write::Writer::new(v2::ChecksumAlgorithm::Sha256);
let mut writer = writer.write_manifest(&manifest)?.write_atoms(atoms)?;
let mut root = v2::write::Directory::default();
for entry in reader.iter_volumes() {
let (name, section) = entry?;
let volume: v3::write::Directory<'static> = section.root()?.try_into()?;
let volume = v3_to_v2_directory(volume);
if name == "metadata" {
writer.write_volume("metadata", volume)?;
} else {
let path_segments = name.to_path_segments()?;
let segments: Vec<_> = path_segments.iter().collect();
let mut directory = volume;
for (index, segment) in segments.iter().enumerate().rev() {
if index != 0 {
let mut temp = v2::write::Directory::default();
temp.children
.insert((*segment).clone(), v2::write::DirEntry::Dir(directory));
directory = temp;
} else {
root.children
.insert((*segment).clone(), v2::write::DirEntry::Dir(directory));
break;
}
}
}
}
writer.write_volume("atom", root)?;
writer
.finish(v2::SignatureAlgorithm::None)
.map(OwnedBuffer::from)
.map_err(anyhow::Error::from)
}
#[cfg(test)]
mod tests {
use sha2::Digest;
use shared_buffer::OwnedBuffer;
use tempfile::TempDir;
use crate::{
metadata::annotations::FileSystemMapping, migration::v2_to_v3, wasmer_package::Package,
Container,
};
use super::v3_to_v2;
#[test]
fn migration_roundtrip() {
let temp = TempDir::new().unwrap();
let wasmer_toml = r#"
[package]
name = "some/package"
version = "0.0.0"
description = "Test package"
[fs]
"/first" = "first"
second = "nested/dir"
"second/child" = "third"
empty = "empty"
"#;
let manifest = temp.path().join("wasmer.toml");
std::fs::write(&manifest, wasmer_toml).unwrap();
let first = temp.path().join("first");
std::fs::create_dir_all(&first).unwrap();
std::fs::write(first.join("file.txt"), "File").unwrap();
let second = temp.path().join("nested").join("dir");
std::fs::create_dir_all(&second).unwrap();
std::fs::write(second.join("README.md"), "please").unwrap();
let another_dir = temp.path().join("nested").join("dir").join("another-dir");
std::fs::create_dir_all(&another_dir).unwrap();
std::fs::write(another_dir.join("empty.txt"), "").unwrap();
let third = temp.path().join("third");
std::fs::create_dir_all(&third).unwrap();
std::fs::write(third.join("file.txt"), "Hello, World!").unwrap();
let empty_dir = temp.path().join("empty");
std::fs::create_dir_all(empty_dir).unwrap();
let package = Package::from_manifest(manifest).unwrap();
let webc = package.serialize().unwrap();
let webc_v2 = v3_to_v2(webc).unwrap();
let container = Container::from_bytes(webc_v2.clone().into_bytes()).unwrap();
let manifest = container.manifest();
let fs_table = manifest.filesystem().unwrap().unwrap();
assert_eq!(
fs_table,
[
FileSystemMapping {
from: None,
volume_name: "atom".to_string(),
host_path: Some("/first".to_string()),
mount_path: "/first".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "atom".to_string(),
host_path: Some("/nested/dir".to_string()),
mount_path: "/second".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "atom".to_string(),
host_path: Some("/third".to_string()),
mount_path: "/second/child".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "atom".to_string(),
host_path: Some("/empty".to_string()),
mount_path: "/empty".to_string(),
},
]
);
let atom_volume = container.get_volume("atom").unwrap();
assert_eq!(
atom_volume.read_file("/first/file.txt").unwrap(),
(OwnedBuffer::from(b"File".as_slice()), None)
);
assert_eq!(
atom_volume.read_file("/nested/dir/README.md").unwrap(),
(OwnedBuffer::from(b"please".as_slice()), None),
);
assert_eq!(
atom_volume
.read_file("/nested/dir/another-dir/empty.txt")
.unwrap(),
(OwnedBuffer::from(b"".as_slice()), None)
);
assert_eq!(
atom_volume.read_file("/third/file.txt").unwrap(),
(OwnedBuffer::from(b"Hello, World!".as_slice()), None)
);
assert_eq!(
atom_volume.read_dir("/empty").unwrap().len(),
0,
"Directories should be included, even if empty"
);
let webc_v3 = v2_to_v3(webc_v2).unwrap();
let container = Container::from_bytes(webc_v3.into_bytes()).unwrap();
let manifest = container.manifest();
let fs_table = manifest.filesystem().unwrap().unwrap();
assert_eq!(
fs_table,
[
FileSystemMapping {
from: None,
volume_name: "/first".to_string(),
host_path: None,
mount_path: "/first".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/nested/dir".to_string(),
host_path: None,
mount_path: "/second".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/third".to_string(),
host_path: None,
mount_path: "/second/child".to_string(),
},
FileSystemMapping {
from: None,
volume_name: "/empty".to_string(),
host_path: None,
mount_path: "/empty".to_string(),
},
]
);
let first_file_hash: [u8; 32] = sha2::Sha256::digest(b"File").into();
let readme_hash: [u8; 32] = sha2::Sha256::digest(b"please").into();
let empty_hash: [u8; 32] = sha2::Sha256::digest(b"").into();
let third_file_hash: [u8; 32] = sha2::Sha256::digest(b"Hello, World!").into();
let first_volume = container.get_volume("/first").unwrap();
assert_eq!(
first_volume.read_file("/file.txt").unwrap(),
(b"File".as_slice().into(), Some(first_file_hash)),
);
let nested_dir_volume = container.get_volume("/nested/dir").unwrap();
assert_eq!(
nested_dir_volume.read_file("README.md").unwrap(),
(b"please".as_slice().into(), Some(readme_hash)),
);
assert_eq!(
nested_dir_volume
.read_file("/another-dir/empty.txt")
.unwrap(),
(b"".as_slice().into(), Some(empty_hash))
);
let third_volume = container.get_volume("/third").unwrap();
assert_eq!(
third_volume.read_file("/file.txt").unwrap(),
(b"Hello, World!".as_slice().into(), Some(third_file_hash))
);
let empty_volume = container.get_volume("/empty").unwrap();
assert_eq!(
empty_volume.read_dir("/").unwrap().len(),
0,
"Directories should be included, even if empty"
);
}
}