summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-02-27 06:16:23 +0800
committer魏曹先生 <1992414357@qq.com>2026-02-27 06:16:23 +0800
commit748c8a3353df887ee4b01e0e1327aa95c1c7225a (patch)
treeab84ee3fd6af53e8b03e73f9bdc5177f10606e4a
parent53141f612ab43136b4b1db7406ac71bb97284460 (diff)
Add remote flag to IndexSource and parsing support
-rw-r--r--systems/sheet/macros/src/lib.rs167
-rw-r--r--systems/sheet/src/index_source.rs72
-rw-r--r--systems/sheet/src/mapping.rs10
-rw-r--r--systems/sheet/src/mapping/parse.rs189
-rw-r--r--systems/sheet/src/mapping/parse_test.rs252
-rw-r--r--systems/sheet/src/sheet.rs38
-rw-r--r--systems/sheet/src/sheet/error.rs14
-rw-r--r--systems/sheet/src/sheet/v1/constants.rs6
-rw-r--r--systems/sheet/src/sheet/v1/reader.rs20
-rw-r--r--systems/sheet/src/sheet/v1/test.rs950
-rw-r--r--systems/sheet/src/sheet/v1/writer.rs18
11 files changed, 1286 insertions, 450 deletions
diff --git a/systems/sheet/macros/src/lib.rs b/systems/sheet/macros/src/lib.rs
index c06f25e..2990f7e 100644
--- a/systems/sheet/macros/src/lib.rs
+++ b/systems/sheet/macros/src/lib.rs
@@ -35,12 +35,22 @@ fn parse_sheet_path(input: &str) -> Result<(String, Vec<String>), String> {
Ok((sheet, path_parts))
}
-/// Parse strings in the format "id/ver"
-fn parse_id_version(input: &str) -> Result<(u32, u16), String> {
- let parts: Vec<&str> = input.split('/').collect();
+/// Parse strings in the format "id/ver" or "~id/ver"
+/// Returns (remote, id, ver)
+fn parse_id_version(input: &str) -> Result<(bool, u32, u16), String> {
+ let trimmed = input.trim();
+
+ // Check if it starts with ~ for local
+ let (remote, id_part) = if trimmed.starts_with('~') {
+ (false, &trimmed[1..])
+ } else {
+ (true, trimmed)
+ };
+
+ let parts: Vec<&str> = id_part.split('/').collect();
if parts.len() != 2 {
return Err(format!(
- "Invalid id/version syntax. Expected: id/ver, got: {}",
+ "Invalid id/version syntax. Expected: id/ver or ~id/ver, got: {}",
input
));
}
@@ -62,7 +72,7 @@ fn parse_id_version(input: &str) -> Result<(u32, u16), String> {
.parse::<u16>()
.map_err(|e| format!("Failed to parse version as u16: {}", e))?;
- Ok((id, ver))
+ Ok((remote, id, ver))
}
/// Parse a path string into a vector of strings
@@ -113,7 +123,7 @@ pub fn mapping_buf(input: TokenStream) -> TokenStream {
}
};
- let (id, ver) = match parse_id_version(right) {
+ let (remote, id, ver) = match parse_id_version(right) {
Ok(result) => result,
Err(err) => {
return syn::Error::new(Span::call_site(), err)
@@ -133,7 +143,7 @@ pub fn mapping_buf(input: TokenStream) -> TokenStream {
#mapping_buf_path::new(
#sheet.to_string(),
#path_vec_tokens,
- #index_source_path::new(#id, #ver)
+ #index_source_path::new(#remote, #id, #ver)
)
};
@@ -176,7 +186,7 @@ pub fn mapping(input: TokenStream) -> TokenStream {
}
};
- let (id, ver) = match parse_id_version(right) {
+ let (remote, id, ver) = match parse_id_version(right) {
Ok(result) => result,
Err(err) => {
return syn::Error::new(Span::call_site(), err)
@@ -195,7 +205,7 @@ pub fn mapping(input: TokenStream) -> TokenStream {
#mapping_path::new(
#sheet,
#path,
- #index_source_path::new(#id, #ver)
+ #index_source_path::new(#remote, #id, #ver)
)
};
@@ -203,9 +213,10 @@ pub fn mapping(input: TokenStream) -> TokenStream {
}
enum LocalMappingParts {
- Latest(String, u32, u16),
- Version(String, u32, u16),
- WithRef(String, u32, u16, String),
+ Latest(String, bool, u32, u16),
+ Version(String, bool, u32, u16),
+ WithRef(String, bool, u32, u16, String),
+ VersionForward(String, bool, u32, u16, u16),
}
impl LocalMappingParts {
@@ -221,13 +232,54 @@ impl LocalMappingParts {
));
}
- // When both "==" and "=>" appear
+ // Check for "=>" followed by "==" syntax: "path" => "id/ver" == "ver2"
+ if input_str.contains("=>") && input_str.contains("==") {
+ // Count occurrences to determine the pattern
+ let arrow_count = input_str.matches("=>").count();
+ let equal_count = input_str.matches("==").count();
+
+ if arrow_count == 1 && equal_count == 1 {
+ // Try to parse as "path" => "id/ver" == "ver2"
+ let parts: Vec<&str> = input_str.split("=>").collect();
+ if parts.len() == 2 {
+ let left = parts[0].trim().trim_matches('"').trim();
+ let right_part = parts[1].trim();
+
+ // Split the right part by "=="
+ let right_parts: Vec<&str> = right_part.split("==").collect();
+ if right_parts.len() == 2 {
+ let middle = right_parts[0].trim().trim_matches('"').trim();
+ let version_str = right_parts[1].trim().trim_matches('"').trim();
+
+ let (remote, id, ver) = parse_id_version(middle)
+ .map_err(|err| syn::Error::new(Span::call_site(), err))?;
+
+ let target_ver = version_str.parse::<u16>().map_err(|err| {
+ syn::Error::new(
+ Span::call_site(),
+ format!("Failed to parse target version as u16: {}", err),
+ )
+ })?;
+
+ return Ok(LocalMappingParts::VersionForward(
+ left.to_string(),
+ remote,
+ id,
+ ver,
+ target_ver,
+ ));
+ }
+ }
+ }
+ }
+
+ // When both "==" and "=>" appear but not in the expected pattern
// It's impossible to determine whether to match the current version or point to a Ref
// Should report an error
if input_str.contains("==") && input_str.contains("=>") {
return Err(syn::Error::new(
Span::call_site(),
- "Ambiguous forward direction. Use either '==' for version or '=>' for ref, not both.",
+ "Ambiguous forward direction. Use either '==' for version or '=>' for ref, or use 'path' => 'id/ver' == 'ver2' syntax.",
));
}
@@ -243,10 +295,15 @@ impl LocalMappingParts {
let left = parts[0].trim().trim_matches('"').trim();
let right = parts[1].trim().trim_matches('"').trim();
- let (id, ver) =
+ let (remote, id, ver) =
parse_id_version(right).map_err(|err| syn::Error::new(Span::call_site(), err))?;
- return Ok(LocalMappingParts::Version(left.to_string(), id, ver));
+ return Ok(LocalMappingParts::Version(
+ left.to_string(),
+ remote,
+ id,
+ ver,
+ ));
}
let parts: Vec<&str> = input_str.split("=>").collect();
@@ -257,30 +314,44 @@ impl LocalMappingParts {
let left = parts[0].trim().trim_matches('"').trim();
let right = parts[1].trim().trim_matches('"').trim();
- let (id, ver) = parse_id_version(right)
+ let (remote, id, ver) = parse_id_version(right)
.map_err(|err| syn::Error::new(Span::call_site(), err))?;
- Ok(LocalMappingParts::Latest(left.to_string(), id, ver))
+ Ok(LocalMappingParts::Latest(left.to_string(), remote, id, ver))
}
3 => {
- // local_mapping!("path" => "id/ver" => "ref") - Ref
+ // Check if the third part is a ref (string) or a version number (u16)
let left = parts[0].trim().trim_matches('"').trim();
let middle = parts[1].trim().trim_matches('"').trim();
let right = parts[2].trim().trim_matches('"').trim();
- let (id, ver) = parse_id_version(middle)
+ let (remote, id, ver) = parse_id_version(middle)
.map_err(|err| syn::Error::new(Span::call_site(), err))?;
- Ok(LocalMappingParts::WithRef(
- left.to_string(),
- id,
- ver,
- right.to_string(),
- ))
+ // Try to parse right as u16 (version number)
+ if let Ok(target_ver) = right.parse::<u16>() {
+ // This is "path" => "id/ver" => "ver2" syntax
+ Ok(LocalMappingParts::VersionForward(
+ left.to_string(),
+ remote,
+ id,
+ ver,
+ target_ver,
+ ))
+ } else {
+ // This is "path" => "id/ver" => "ref" syntax
+ Ok(LocalMappingParts::WithRef(
+ left.to_string(),
+ remote,
+ id,
+ ver,
+ right.to_string(),
+ ))
+ }
}
_ => Err(syn::Error::new(
Span::call_site(),
- "Invalid local_mapping syntax. Expected: local_mapping!(\"path\" => \"id/ver\") or local_mapping!(\"path\" == \"id/ver\") or local_mapping!(\"path\" => \"id/ver\" => \"ref\")",
+ "Invalid local_mapping syntax. Expected: local_mapping!(\"path\" => \"id/ver\") or local_mapping!(\"path\" == \"id/ver\") or local_mapping!(\"path\" => \"id/ver\" => \"ref\") or local_mapping!(\"path\" => \"id/ver\" => \"ver2\") or local_mapping!(\"path\" => \"id/ver\" == \"ver2\")",
)),
}
}
@@ -310,6 +381,18 @@ impl LocalMappingParts {
/// // and expects to match the version declared in `ref`
/// "your_dir/your_file.suffix" => "index_id/version" => "ref"
/// );
+///
+/// let lcoal_mapping_version_forward = local_mapping!(
+/// // Map the `version` of index `index_id`
+/// // to `your_dir/your_file.suffix`
+/// // but expects to point to a specific version `ver2`
+/// "your_dir/your_file.suffix" => "index_id/version" => "ver2"
+/// );
+///
+/// let lcoal_mapping_version_forward_alt = local_mapping!(
+/// // Alternative syntax for the same behavior
+/// "your_dir/your_file.suffix" => "index_id/version" == "ver2"
+/// );
/// ```
#[proc_macro]
pub fn local_mapping(input: TokenStream) -> TokenStream {
@@ -326,44 +409,44 @@ pub fn local_mapping(input: TokenStream) -> TokenStream {
parse_str(INDEX_SOURCE).expect("Failed to parse INDEX_SOURCE");
match parts {
- LocalMappingParts::Latest(path_str, id, ver) => {
+ LocalMappingParts::Latest(path_str, remote, id, ver) => {
let path_vec = parse_path_string(&path_str);
let path_vec_tokens = path_vec_to_tokens(&path_vec);
let expanded = quote! {
#local_mapping_path::new(
#path_vec_tokens,
- #index_source_path::new(#id, #ver),
+ #index_source_path::new(#remote, #id, #ver),
#local_mapping_forward_path::Latest
)
};
expanded.into()
}
- LocalMappingParts::Version(path_str, id, ver) => {
+ LocalMappingParts::Version(path_str, remote, id, ver) => {
let path_vec = parse_path_string(&path_str);
let path_vec_tokens = path_vec_to_tokens(&path_vec);
let expanded = quote! {
#local_mapping_path::new(
#path_vec_tokens,
- #index_source_path::new(#id, #ver),
+ #index_source_path::new(#remote, #id, #ver),
#local_mapping_forward_path::Version {
- version_name: #ver.to_string()
+ version: #ver
}
)
};
expanded.into()
}
- LocalMappingParts::WithRef(path_str, id, ver, ref_name) => {
+ LocalMappingParts::WithRef(path_str, remote, id, ver, ref_name) => {
let path_vec = parse_path_string(&path_str);
let path_vec_tokens = path_vec_to_tokens(&path_vec);
let expanded = quote! {
#local_mapping_path::new(
#path_vec_tokens,
- #index_source_path::new(#id, #ver),
+ #index_source_path::new(#remote, #id, #ver),
#local_mapping_forward_path::Ref {
sheet_name: #ref_name.to_string()
}
@@ -372,5 +455,21 @@ pub fn local_mapping(input: TokenStream) -> TokenStream {
expanded.into()
}
+ LocalMappingParts::VersionForward(path_str, remote, id, ver, target_ver) => {
+ let path_vec = parse_path_string(&path_str);
+ let path_vec_tokens = path_vec_to_tokens(&path_vec);
+
+ let expanded = quote! {
+ #local_mapping_path::new(
+ #path_vec_tokens,
+ #index_source_path::new(#remote, #id, #ver),
+ #local_mapping_forward_path::Version {
+ version: #target_ver
+ }
+ )
+ };
+
+ expanded.into()
+ }
}
}
diff --git a/systems/sheet/src/index_source.rs b/systems/sheet/src/index_source.rs
index b22f5a6..e322670 100644
--- a/systems/sheet/src/index_source.rs
+++ b/systems/sheet/src/index_source.rs
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
/// Points to a unique resource address in Vault
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct IndexSource {
+ remote: bool,
+
/// The index ID of the resource
id: u32,
@@ -15,8 +17,31 @@ pub struct IndexSource {
impl IndexSource {
/// Create IndexSource
- pub fn new(id: u32, ver: u16) -> Self {
- IndexSource { id, ver }
+ pub fn new(remote: bool, id: u32, ver: u16) -> Self {
+ IndexSource { remote, id, ver }
+ }
+
+ /// Create IndexSource To Local Namespace
+ pub fn new_local(id: u32, ver: u16) -> Self {
+ IndexSource {
+ remote: false,
+ id,
+ ver,
+ }
+ }
+
+ /// Create IndexSource To Remote Namespace
+ pub fn new_remote(id: u32, ver: u16) -> Self {
+ IndexSource {
+ remote: true,
+ id,
+ ver,
+ }
+ }
+
+ /// Check if the IndexSource points to a remote namespace
+ pub fn is_remote(&self) -> bool {
+ self.remote
}
/// Get index ID from IndexSource
@@ -34,7 +59,7 @@ impl IndexSource {
impl PartialEq for IndexSource {
fn eq(&self, other: &Self) -> bool {
- &self.id == &other.id && &self.ver == &other.ver
+ &self.remote == &other.remote && &self.id == &other.id && &self.ver == &other.ver
}
}
@@ -44,6 +69,7 @@ impl Eq for IndexSource {}
impl std::hash::Hash for IndexSource {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.remote.hash(state);
self.id.hash(state);
self.ver.hash(state);
}
@@ -55,9 +81,13 @@ impl<'a> TryFrom<&'a str> for IndexSource {
type Error = &'static str;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
- let parts: Vec<&str> = value.split('/').collect();
+ let trimmed = value.trim();
+ let remote = !trimmed.starts_with('~');
+ let value_without_tilde = if remote { trimmed } else { &trimmed[1..] };
+
+ let parts: Vec<&str> = value_without_tilde.split('/').collect();
if parts.len() != 2 {
- return Err("Invalid format: expected 'id/version'");
+ return Err("Invalid format: expected '[~]id/version'");
}
let id_str = parts[0].trim();
@@ -74,9 +104,7 @@ impl<'a> TryFrom<&'a str> for IndexSource {
.parse::<u16>()
.map_err(|_| "Version must be a valid u16")?;
- // Check for overflow (though parsing already validates range)
- // Additional bounds checks can be added here if needed
- Ok(Self { id, ver })
+ Ok(Self { remote, id, ver })
}
}
@@ -88,9 +116,9 @@ impl TryFrom<String> for IndexSource {
}
}
-impl From<IndexSource> for (u32, u16) {
+impl From<IndexSource> for (bool, u32, u16) {
fn from(src: IndexSource) -> Self {
- (src.id, src.ver)
+ (src.remote, src.id, src.ver)
}
}
@@ -105,12 +133,34 @@ impl IndexSource {
pub fn set_version(&mut self, version: u16) {
self.ver = version;
}
+
+ /// Set the remote flag of IndexSource
+ pub fn set_is_remote(&mut self, remote: bool) {
+ self.remote = remote;
+ }
+
+ /// Convert IndexSource to local namespace
+ pub fn to_local(&mut self) {
+ self.remote = false;
+ }
+
+ /// Convert IndexSource to remote namespace
+ pub fn to_remote(&mut self) {
+ self.remote = true;
+ }
}
// Implement Display for IndexSourceBuf and IndexSource
impl std::fmt::Display for IndexSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}/{}", self.id.to_string(), self.ver.to_string())
+ let local_symbol = if self.remote { "" } else { "~" };
+ write!(
+ f,
+ "{}{}/{}",
+ local_symbol,
+ self.id.to_string(),
+ self.ver.to_string()
+ )
}
}
diff --git a/systems/sheet/src/mapping.rs b/systems/sheet/src/mapping.rs
index f509c0b..3dfb67e 100644
--- a/systems/sheet/src/mapping.rs
+++ b/systems/sheet/src/mapping.rs
@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
use crate::{index_source::IndexSource, mapping::error::ParseMappingError};
pub mod error;
+pub mod parse;
+pub mod parse_test;
// Validation rules for LocalMapping
// LocalMapping is a key component for writing and reading SheetData
@@ -438,6 +440,14 @@ impl std::fmt::Display for LocalMapping {
}
}
+impl TryFrom<String> for LocalMapping {
+ type Error = ParseMappingError;
+
+ fn try_from(s: String) -> Result<Self, Self::Error> {
+ s.as_str().try_into()
+ }
+}
+
// Implement editing functionality for MappingBuf and LocalMapping
impl MappingBuf {
diff --git a/systems/sheet/src/mapping/parse.rs b/systems/sheet/src/mapping/parse.rs
new file mode 100644
index 0000000..e203c96
--- /dev/null
+++ b/systems/sheet/src/mapping/parse.rs
@@ -0,0 +1,189 @@
+use just_fmt::fmt_path::fmt_path_str;
+
+use crate::{
+ index_source::IndexSource,
+ mapping::{LocalMapping, LocalMappingForward, error::ParseMappingError},
+};
+
+impl TryFrom<&str> for LocalMapping {
+ type Error = ParseMappingError;
+
+ fn try_from(s: &str) -> Result<Self, Self::Error> {
+ // Remove surrounding quotes if present
+ let s = s.trim_matches('"');
+
+ // Helper function to remove quotes from a string
+ fn remove_quotes(s: &str) -> String {
+ // Simply remove all quotes from the string
+ s.replace('"', "").trim().to_string()
+ }
+
+ // Helper function to split by operator, handling both spaced and non-spaced versions
+ fn split_by_operator<'a>(s: &'a str, operator: &'a str) -> Vec<&'a str> {
+ let mut result = Vec::new();
+ let mut start = 0;
+
+ // Find all occurrences of the operator
+ let mut search_from = 0;
+ while let Some(pos) = s[search_from..].find(operator) {
+ let actual_pos = search_from + pos;
+ result.push(&s[start..actual_pos]);
+ start = actual_pos + operator.len();
+ search_from = start;
+ }
+
+ if start < s.len() {
+ result.push(&s[start..]);
+ }
+
+ result
+ }
+
+ // Helper function to find operator position
+ fn find_operator<'a>(s: &'a str, operator: &'a str) -> Option<usize> {
+ s.find(operator)
+ }
+
+ // Try to parse "path" => "source" == "version" pattern
+ if let Some(arrow_pos) = find_operator(s, "=>") {
+ let after_arrow = &s[arrow_pos + 2..];
+ if let Some(equal_pos) = find_operator(after_arrow, "==") {
+ // Format: "path" => "source" == "version"
+ let path = remove_quotes(s[..arrow_pos].trim());
+ let middle_part = after_arrow[..equal_pos].trim();
+ let version_part = after_arrow[equal_pos + 2..].trim();
+
+ let middle = remove_quotes(middle_part);
+ let version_part_str = remove_quotes(version_part);
+
+ let val = fmt_path_str(path)
+ .map_err(|_| ParseMappingError::InvalidMapping)?
+ .split('/')
+ .map(|s| s.to_string())
+ .collect();
+
+ let source = IndexSource::try_from(middle.as_str())
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ let version = version_part_str
+ .parse::<u16>()
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ return Ok(LocalMapping {
+ val,
+ source,
+ forward: LocalMappingForward::Version { version },
+ });
+ }
+ }
+
+ // Split by "=>" to parse the format
+ let parts = split_by_operator(s, "=>");
+
+ match parts.len() {
+ 1 => {
+ // Check for "==" operator
+ if let Some(equal_pos) = find_operator(s, "==") {
+ // Format: "path" == "source" (when mapped_version equals version)
+ let path_raw = s[..equal_pos].trim();
+ let source_part_raw = s[equal_pos + 2..].trim();
+ let path = remove_quotes(path_raw);
+ let source_str = remove_quotes(source_part_raw);
+
+ let val = fmt_path_str(path)
+ .map_err(|_| ParseMappingError::InvalidMapping)?
+ .split('/')
+ .map(|s| s.to_string())
+ .collect();
+
+ let source = IndexSource::try_from(source_str.as_str())
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ let version = source.version();
+
+ Ok(LocalMapping {
+ val,
+ source,
+ forward: LocalMappingForward::Version { version },
+ })
+ } else {
+ Err(ParseMappingError::InvalidMapping)
+ }
+ }
+ 2 => {
+ // Check if the second part contains "=="
+ if let Some(equal_pos) = find_operator(parts[1], "==") {
+ // Format: "path" => "source" == "version"
+ let path = remove_quotes(parts[0].trim());
+ let middle_part = parts[1][..equal_pos].trim();
+ let version_part = parts[1][equal_pos + 2..].trim();
+
+ let middle = remove_quotes(middle_part);
+ let version_part_str = remove_quotes(version_part);
+
+ let val = fmt_path_str(path)
+ .map_err(|_| ParseMappingError::InvalidMapping)?
+ .split('/')
+ .map(|s| s.to_string())
+ .collect();
+
+ let source = IndexSource::try_from(middle.as_str())
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ let version = version_part_str
+ .parse::<u16>()
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ return Ok(LocalMapping {
+ val,
+ source,
+ forward: LocalMappingForward::Version { version },
+ });
+ }
+
+ // Format: "path" => "source"
+ let path = remove_quotes(parts[0].trim());
+ let source_str = remove_quotes(parts[1].trim());
+
+ let val = fmt_path_str(path)
+ .map_err(|_| ParseMappingError::InvalidMapping)?
+ .split('/')
+ .map(|s| s.to_string())
+ .collect();
+
+ let source = IndexSource::try_from(source_str.as_str())
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ Ok(LocalMapping {
+ val,
+ source,
+ forward: LocalMappingForward::Latest,
+ })
+ }
+ 3 => {
+ // Format: "path" => "source" => "sheet_name"
+ let path = remove_quotes(parts[0].trim());
+ let source_str = remove_quotes(parts[1].trim());
+ let sheet_name = remove_quotes(parts[2].trim());
+
+ let val = fmt_path_str(path)
+ .map_err(|_| ParseMappingError::InvalidMapping)?
+ .split('/')
+ .map(|s| s.to_string())
+ .collect();
+
+ let source = IndexSource::try_from(source_str.as_str())
+ .map_err(|_| ParseMappingError::InvalidMapping)?;
+
+ Ok(LocalMapping {
+ val,
+ source,
+ forward: LocalMappingForward::Ref {
+ sheet_name: sheet_name,
+ },
+ })
+ }
+ _ => Err(ParseMappingError::InvalidMapping),
+ }
+ }
+}
diff --git a/systems/sheet/src/mapping/parse_test.rs b/systems/sheet/src/mapping/parse_test.rs
new file mode 100644
index 0000000..a411360
--- /dev/null
+++ b/systems/sheet/src/mapping/parse_test.rs
@@ -0,0 +1,252 @@
+#[cfg(test)]
+mod tests {
+ use crate::{
+ index_source::IndexSource,
+ mapping::{LocalMapping, LocalMappingForward},
+ };
+
+ /// Helper macro for comparing two LocalMapping instances
+ /// Checks equality of the mappings themselves, their forward fields, and their index sources
+ macro_rules! mapping_eq {
+ ($a:expr, $b:expr) => {
+ assert_eq!($a, $b);
+ assert_eq!($a.forward, $b.forward);
+ assert_eq!($a.index_source(), $b.index_source());
+ };
+ }
+
+ #[test]
+ fn test_local_mapping_parse() {
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Version { version: 2u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" == \"1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Ref {
+ sheet_name: "ref".to_string(),
+ },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"1/2\" => \"ref\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Version { version: 3u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"1/2\" == \"3\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"~1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Version { version: 2u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" == \"~1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Ref {
+ sheet_name: "ref".to_string(),
+ },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"~1/2\" => \"ref\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Version { version: 3u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" => \"~1/2\" == \"3\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Version { version: 2u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"==\"1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Ref {
+ sheet_name: "ref".to_string(),
+ },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"1/2\"=>\"ref\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Version { version: 3u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"1/2\"==\"3\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"~1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Version { version: 2u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"==\"~1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Ref {
+ sheet_name: "ref".to_string(),
+ },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"~1/2\"=>\"ref\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Version { version: 3u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=>\"~1/2\"==\"3\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ //
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" =>\"1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Version { version: 2u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"== \"1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Ref {
+ sheet_name: "ref".to_string(),
+ },
+ )
+ .unwrap();
+ let local_mapping_gen =
+ LocalMapping::try_from("\"A.png\" => \"1/2\" =>\"ref\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(true, 1u32, 2u16),
+ LocalMappingForward::Version { version: 3u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=> \"1/2\" ==\"3\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from(" \"A.png\"=>\"~1/2\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Version { version: 2u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\" ==\"~1/2\" ").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Ref {
+ sheet_name: "ref".to_string(),
+ },
+ )
+ .unwrap();
+ let local_mapping_gen =
+ LocalMapping::try_from("\"A.png\"=> \"~1/2\"=> \"ref\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+
+ let local_mapping = LocalMapping::new(
+ vec!["A.png".to_string()],
+ IndexSource::new(false, 1u32, 2u16),
+ LocalMappingForward::Version { version: 3u16 },
+ )
+ .unwrap();
+ let local_mapping_gen = LocalMapping::try_from("\"A.png\"=> \"~1/2\" ==\"3\"").unwrap();
+ mapping_eq!(local_mapping, local_mapping_gen);
+ }
+}
diff --git a/systems/sheet/src/sheet.rs b/systems/sheet/src/sheet.rs
index 07d284b..e7a130d 100644
--- a/systems/sheet/src/sheet.rs
+++ b/systems/sheet/src/sheet.rs
@@ -13,7 +13,7 @@ use crate::{
index_source::IndexSource,
mapping::{LocalMapping, LocalMappingForward, Mapping, MappingBuf},
sheet::{
- error::{ReadSheetDataError, SheetApplyError, SheetEditError},
+ error::{ParseSheetError, ReadSheetDataError, SheetApplyError, SheetEditError},
reader::{read_mapping, read_sheet_data},
writer::convert_sheet_data_to_bytes,
},
@@ -486,6 +486,42 @@ impl SheetData {
}
}
+impl std::fmt::Display for SheetData {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut vec = self
+ .mappings()
+ .iter()
+ .cloned()
+ .collect::<Vec<LocalMapping>>();
+ vec.sort();
+ write!(
+ f,
+ "{}",
+ vec.iter()
+ .map(|m| m.to_string())
+ .collect::<Vec<_>>()
+ .join("\n")
+ )
+ }
+}
+
+impl TryFrom<&str> for SheetData {
+ type Error = ParseSheetError;
+
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ let mut sheet = SheetData::empty().pack("temp");
+ for line in value.split("\n") {
+ if line.trim().is_empty() {
+ continue;
+ }
+ let mapping = LocalMapping::try_from(line)?;
+ let _ = sheet.insert_mapping(mapping)?;
+ }
+ let _ = sheet.apply()?;
+ Ok(sheet.unpack())
+ }
+}
+
impl From<SheetData> for Vec<u8> {
fn from(value: SheetData) -> Self {
value.as_bytes()
diff --git a/systems/sheet/src/sheet/error.rs b/systems/sheet/src/sheet/error.rs
index 3e5661a..3142a6d 100644
--- a/systems/sheet/src/sheet/error.rs
+++ b/systems/sheet/src/sheet/error.rs
@@ -1,3 +1,5 @@
+use crate::mapping::error::ParseMappingError;
+
#[derive(Debug, thiserror::Error)]
pub enum SheetEditError {
#[error("Edit Failed: Node already exists: `{0}`")]
@@ -17,6 +19,18 @@ pub enum SheetApplyError {
}
#[derive(Debug, thiserror::Error)]
+pub enum ParseSheetError {
+ #[error("Parse mapping error: {0}")]
+ ParseMappingError(#[from] ParseMappingError),
+
+ #[error("Sheet edit error: {0}")]
+ SheetEditError(#[from] SheetEditError),
+
+ #[error("Sheet apply error: {0}")]
+ SheetApplyError(#[from] SheetApplyError),
+}
+
+#[derive(Debug, thiserror::Error)]
pub enum ReadSheetDataError {
#[error("IO error: {0}")]
IOErr(#[from] std::io::Error),
diff --git a/systems/sheet/src/sheet/v1/constants.rs b/systems/sheet/src/sheet/v1/constants.rs
index 69714bb..7073278 100644
--- a/systems/sheet/src/sheet/v1/constants.rs
+++ b/systems/sheet/src/sheet/v1/constants.rs
@@ -44,12 +44,16 @@ pub const MAPPING_BUCKET_MIN_SIZE: usize = 0
+ 2 // INDEX_OFFSET
;
-// Index Table (6: 4 + 2)
+// Index Table (10: 4 + 2 + 1 + 3)
//
// [INDEX_ID: u32]
// [INDEX_VERSION: u16]
+// [REMOTE_FLAG: u8]
+// [RESERVED: u8; 3]
pub const INDEX_ENTRY_SIZE: usize = 0
+ 4 // INDEX_ID
+ 2 // INDEX_VERSION
+ + 1 // REMOTE_FLAG
+ + 3 // RESERVED
;
diff --git a/systems/sheet/src/sheet/v1/reader.rs b/systems/sheet/src/sheet/v1/reader.rs
index e23e91b..66d8914 100644
--- a/systems/sheet/src/sheet/v1/reader.rs
+++ b/systems/sheet/src/sheet/v1/reader.rs
@@ -257,8 +257,9 @@ fn read_index_table(
let id = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
let ver = u16::from_le_bytes([data[pos + 4], data[pos + 5]]);
+ let remote = data[pos + 6] != 0; // 0 = local, non-zero = remote
- sources.push(IndexSource::new(id, ver));
+ sources.push(IndexSource::new(remote, id, ver));
pos += INDEX_ENTRY_SIZE;
}
@@ -510,17 +511,26 @@ mod tests {
#[test]
fn test_read_index_table() {
let mut data = Vec::new();
+ // First entry: local source
data.extend_from_slice(&123u32.to_le_bytes());
data.extend_from_slice(&456u16.to_le_bytes());
+ data.push(0); // remote flag (0 = local)
+ data.extend_from_slice(&[0u8; 3]); // reserved bytes
+
+ // Second entry: remote source
data.extend_from_slice(&789u32.to_le_bytes());
data.extend_from_slice(&1011u16.to_le_bytes());
+ data.push(1); // remote flag (1 = remote)
+ data.extend_from_slice(&[0u8; 3]); // reserved bytes
let sources = read_index_table(&data, 0, 2).unwrap();
assert_eq!(sources.len(), 2);
assert_eq!(sources[0].id(), 123);
assert_eq!(sources[0].version(), 456);
+ assert_eq!(sources[0].is_remote(), false);
assert_eq!(sources[1].id(), 789);
assert_eq!(sources[1].version(), 1011);
+ assert_eq!(sources[1].is_remote(), true);
}
#[test]
@@ -544,7 +554,7 @@ mod tests {
bucket_data.extend_from_slice(path2); // KEY_BYTES
bucket_data.extend_from_slice(&1u32.to_le_bytes()); // INDEX_OFFSET
- let index_sources = vec![IndexSource::new(1, 1), IndexSource::new(2, 1)];
+ let index_sources = vec![IndexSource::new_local(1, 1), IndexSource::new_local(2, 1)];
let mappings = read_bucket_data(&bucket_data, &index_sources).unwrap();
assert_eq!(mappings.len(), 2);
@@ -574,21 +584,21 @@ mod tests {
// Add mappings that will go to different buckets
let mapping1 = crate::mapping::LocalMapping::new(
vec!["aaa".to_string(), "file1.txt".to_string()],
- crate::index_source::IndexSource::new(1, 1),
+ crate::index_source::IndexSource::new_local(1, 1),
crate::mapping::LocalMappingForward::Latest,
)
.unwrap();
let mapping2 = crate::mapping::LocalMapping::new(
vec!["mmm".to_string(), "file2.txt".to_string()],
- crate::index_source::IndexSource::new(2, 2),
+ crate::index_source::IndexSource::new_local(2, 2),
crate::mapping::LocalMappingForward::Latest,
)
.unwrap();
let mapping3 = crate::mapping::LocalMapping::new(
vec!["zzz".to_string(), "file3.txt".to_string()],
- crate::index_source::IndexSource::new(3, 3),
+ crate::index_source::IndexSource::new_local(3, 3),
crate::mapping::LocalMappingForward::Latest,
)
.unwrap();
diff --git a/systems/sheet/src/sheet/v1/test.rs b/systems/sheet/src/sheet/v1/test.rs
index dfba3c8..995bcf9 100644
--- a/systems/sheet/src/sheet/v1/test.rs
+++ b/systems/sheet/src/sheet/v1/test.rs
@@ -11,450 +11,616 @@ use crate::{
use std::collections::HashSet;
use std::fs;
-/// Test writing and re-reading sheet data
-#[test]
-fn test_sheet_data_roundtrip() {
- // Create test data
- let _sheet_data = SheetData::empty();
-
- // Create some test mappings
- let mapping1 = LocalMapping::new(
- vec!["src".to_string(), "main.rs".to_string()],
- IndexSource::new(1001, 1),
- LocalMappingForward::Latest,
- )
- .unwrap();
-
- let mapping2 = LocalMapping::new(
- vec!["docs".to_string(), "README.md".to_string()],
- IndexSource::new(1002, 2),
- LocalMappingForward::Ref {
- sheet_name: "reference".to_string(),
- },
- )
- .unwrap();
-
- let mapping3 = LocalMapping::new(
- vec![
- "assets".to_string(),
- "images".to_string(),
- "logo.png".to_string(),
- ],
- IndexSource::new(1003, 3),
- LocalMappingForward::Version { version: 12345 },
- )
- .unwrap();
-
- // Add mappings to SheetData
- // Note: Since the mappings field of SheetData is private, we need to create SheetData in another way
- // Here we directly create a new HashSet
- let mut mappings = HashSet::new();
- mappings.insert(mapping1.clone());
- mappings.insert(mapping2.clone());
- mappings.insert(mapping3.clone());
-
- let sheet_data = SheetData { mappings };
-
- // Convert SheetData to bytes
- let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
-
- // Verify byte data is not empty
- assert!(!bytes.is_empty(), "Converted bytes should not be empty");
-
- // Verify file header
- assert_eq!(bytes[0], 1, "Sheet version should be 1");
-
- // Re-read SheetData from bytes
- let restored_sheet_data =
- read_sheet_data(&bytes).expect("Failed to read sheet data from bytes");
-
- // Verify mapping count
- assert_eq!(
- restored_sheet_data.mappings.len(),
- sheet_data.mappings.len(),
- "Restored sheet should have same number of mappings"
- );
-
- // Verify each mapping exists
- for mapping in &sheet_data.mappings {
- assert!(
- restored_sheet_data.mappings.contains(mapping),
- "Restored sheet should contain mapping: {:?}",
- mapping
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Test writing and re-reading sheet data
+ #[test]
+ fn test_sheet_data_roundtrip() {
+ // Create test data
+ let _sheet_data = SheetData::empty();
+
+ // Create some test mappings
+ let mapping1 = LocalMapping::new(
+ vec!["src".to_string(), "main.rs".to_string()],
+ IndexSource::new_local(1001, 1),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mapping2 = LocalMapping::new(
+ vec!["docs".to_string(), "README.md".to_string()],
+ IndexSource::new_local(1002, 2),
+ LocalMappingForward::Ref {
+ sheet_name: "reference".to_string(),
+ },
+ )
+ .unwrap();
+
+ let mapping3 = LocalMapping::new(
+ vec![
+ "assets".to_string(),
+ "images".to_string(),
+ "logo.png".to_string(),
+ ],
+ IndexSource::new_local(1003, 3),
+ LocalMappingForward::Version { version: 12345 },
+ )
+ .unwrap();
+
+ // Add mappings to SheetData
+ // Note: Since the mappings field of SheetData is private, we need to create SheetData in another way
+ // Here we directly create a new HashSet
+ let mut mappings = HashSet::new();
+ mappings.insert(mapping1.clone());
+ mappings.insert(mapping2.clone());
+ mappings.insert(mapping3.clone());
+
+ let sheet_data = SheetData { mappings };
+
+ // Convert SheetData to bytes
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+
+ // Verify byte data is not empty
+ assert!(!bytes.is_empty(), "Converted bytes should not be empty");
+
+ // Verify file header
+ assert_eq!(bytes[0], 1, "Sheet version should be 1");
+
+ // Re-read SheetData from bytes
+ let restored_sheet_data =
+ read_sheet_data(&bytes).expect("Failed to read sheet data from bytes");
+
+ // Verify mapping count
+ assert_eq!(
+ restored_sheet_data.mappings.len(),
+ sheet_data.mappings.len(),
+ "Restored sheet should have same number of mappings"
);
+
+ // Verify each mapping exists
+ for mapping in &sheet_data.mappings {
+ assert!(
+ restored_sheet_data.mappings.contains(mapping),
+ "Restored sheet should contain mapping: {:?}",
+ mapping
+ );
+ }
+
+ // Verify specific mapping content
+ for mapping in &restored_sheet_data.mappings {
+ // Find original mapping
+ let original_mapping = sheet_data.mappings.get(mapping.value()).unwrap();
+
+ // Verify path
+ assert_eq!(
+ mapping.value(),
+ original_mapping.value(),
+ "Path should match"
+ );
+
+ // Verify index source
+ assert_eq!(
+ mapping.index_source().id(),
+ original_mapping.index_source().id(),
+ "Index source ID should match"
+ );
+
+ assert_eq!(
+ mapping.index_source().version(),
+ original_mapping.index_source().version(),
+ "Index source version should match"
+ );
+
+ // Verify forward information
+ let (original_type, _, _) = original_mapping.forward().unpack();
+ let (restored_type, _, _) = mapping.forward().unpack();
+ assert_eq!(restored_type, original_type, "Forward type should match");
+ }
}
- // Verify specific mapping content
- for mapping in &restored_sheet_data.mappings {
- // Find original mapping
- let original_mapping = sheet_data.mappings.get(mapping.value()).unwrap();
+ /// Test reading and writing empty sheet data
+ #[test]
+ fn test_empty_sheet_roundtrip() {
+ // Create empty SheetData
+ let sheet_data = SheetData::empty();
+
+ // Convert to bytes
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+
+ // Verify file header
+ assert_eq!(bytes.len(), 15, "Empty sheet should have header size only");
+ assert_eq!(bytes[0], 1, "Sheet version should be 1");
- // Verify path
+ // Verify offsets - For empty sheet, mapping data offset and index table offset should be the same
+ let mapping_data_offset =
+ u32::from_le_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]) as usize;
+ let index_table_offset =
+ u32::from_le_bytes([bytes[11], bytes[12], bytes[13], bytes[14]]) as usize;
assert_eq!(
- mapping.value(),
- original_mapping.value(),
- "Path should match"
+ mapping_data_offset, index_table_offset,
+ "For empty sheet, both offsets should be the same"
);
-
- // Verify index source
assert_eq!(
- mapping.index_source().id(),
- original_mapping.index_source().id(),
- "Index source ID should match"
+ mapping_data_offset, HEADER_SIZE,
+ "Offsets should point to end of header"
);
- assert_eq!(
- mapping.index_source().version(),
- original_mapping.index_source().version(),
- "Index source version should match"
+ // Mapping count should be 0
+ let mapping_count = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
+ assert_eq!(mapping_count, 0, "Mapping count should be 0");
+
+ // Index source count should be 0
+ let index_count = u16::from_le_bytes([bytes[5], bytes[6]]);
+ assert_eq!(index_count, 0, "Index count should be 0");
+
+ // Re-read
+ let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read empty sheet data");
+
+ // Verify it's empty
+ assert!(
+ restored_sheet_data.mappings.is_empty(),
+ "Restored empty sheet should have no mappings"
);
+ }
+
+ /// Test reading and writing a single mapping
+ #[test]
+ fn test_single_mapping_roundtrip() {
+ // Create a single mapping
+ let mapping = LocalMapping::new(
+ vec!["test.txt".to_string()],
+ IndexSource::new_local(999, 42),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mut mappings = HashSet::new();
+ mappings.insert(mapping.clone());
- // Verify forward information
- let (original_type, _, _) = original_mapping.forward().unpack();
- let (restored_type, _, _) = mapping.forward().unpack();
- assert_eq!(restored_type, original_type, "Forward type should match");
+ let sheet_data = SheetData { mappings };
+
+ // Convert to bytes
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+
+ // Re-read
+ let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
+
+ // Verify
+ assert_eq!(restored_sheet_data.mappings.len(), 1);
+ let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap();
+
+ assert_eq!(restored_mapping.value(), &["test.txt".to_string()]);
+ assert_eq!(restored_mapping.index_source().id(), 999);
+ assert_eq!(restored_mapping.index_source().version(), 42);
+
+ let (forward_type, _, _) = restored_mapping.forward().unpack();
+ assert_eq!(forward_type, 0); // Latest type id is 0
}
-}
-/// Test reading and writing empty sheet data
-#[test]
-fn test_empty_sheet_roundtrip() {
- // Create empty SheetData
- let sheet_data = SheetData::empty();
-
- // Convert to bytes
- let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
-
- // Verify file header
- assert_eq!(bytes.len(), 15, "Empty sheet should have header size only");
- assert_eq!(bytes[0], 1, "Sheet version should be 1");
-
- // Verify offsets - For empty sheet, mapping data offset and index table offset should be the same
- let mapping_data_offset =
- u32::from_le_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]) as usize;
- let index_table_offset =
- u32::from_le_bytes([bytes[11], bytes[12], bytes[13], bytes[14]]) as usize;
- assert_eq!(
- mapping_data_offset, index_table_offset,
- "For empty sheet, both offsets should be the same"
- );
- assert_eq!(
- mapping_data_offset, HEADER_SIZE,
- "Offsets should point to end of header"
- );
-
- // Mapping count should be 0
- let mapping_count = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
- assert_eq!(mapping_count, 0, "Mapping count should be 0");
-
- // Index source count should be 0
- let index_count = u16::from_le_bytes([bytes[5], bytes[6]]);
- assert_eq!(index_count, 0, "Index count should be 0");
-
- // Re-read
- let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read empty sheet data");
-
- // Verify it's empty
- assert!(
- restored_sheet_data.mappings.is_empty(),
- "Restored empty sheet should have no mappings"
- );
-}
+ /// Test file system read/write
+ #[test]
+ fn test_file_system_roundtrip() {
+ // Create test data
+ let mapping1 = LocalMapping::new(
+ vec!["file0.txt".to_string()],
+ IndexSource::new_local(1, 1),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mapping2 = LocalMapping::new(
+ vec!["dir1".to_string(), "file1.txt".to_string()],
+ IndexSource::new_local(2, 2),
+ LocalMappingForward::Ref {
+ sheet_name: "other".to_string(),
+ },
+ )
+ .unwrap();
+
+ let mapping3 = LocalMapping::new(
+ vec!["dir2".to_string(), "file2.txt".to_string()],
+ IndexSource::new_local(3, 3),
+ LocalMappingForward::Version { version: 35 },
+ )
+ .unwrap();
-/// Test reading and writing a single mapping
-#[test]
-fn test_single_mapping_roundtrip() {
- // Create a single mapping
- let mapping = LocalMapping::new(
- vec!["test.txt".to_string()],
- IndexSource::new(999, 42),
- LocalMappingForward::Latest,
- )
- .unwrap();
+ let mut mappings = HashSet::new();
+ mappings.insert(mapping1.clone());
+ mappings.insert(mapping2.clone());
+ mappings.insert(mapping3.clone());
- let mut mappings = HashSet::new();
- mappings.insert(mapping.clone());
+ let sheet_data = SheetData { mappings };
- let sheet_data = SheetData { mappings };
+ // Convert to bytes
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
- // Convert to bytes
- let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+ // Write to file
+ let test_file_path = ".temp/test.sheet";
+ let test_file_path_hex = ".temp/test_hex.txt";
- // Re-read
- let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
+ // Ensure directory exists
+ if let Some(parent) = std::path::Path::new(test_file_path).parent() {
+ fs::create_dir_all(parent).expect("Failed to create test directory");
+ }
- // Verify
- assert_eq!(restored_sheet_data.mappings.len(), 1);
- let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap();
+ fs::write(test_file_path, &bytes).expect("Failed to write test file");
+ fs::write(test_file_path_hex, hex_display_slice(&bytes))
+ .expect("Failed to write test file");
- assert_eq!(restored_mapping.value(), &["test.txt".to_string()]);
- assert_eq!(restored_mapping.index_source().id(), 999);
- assert_eq!(restored_mapping.index_source().version(), 42);
+ // Read file
+ let file_bytes = fs::read(test_file_path).expect("Failed to read test file");
- let (forward_type, _, _) = restored_mapping.forward().unpack();
- assert_eq!(forward_type, 0); // Latest type id is 0
-}
+ // Verify file content matches original bytes
+ assert_eq!(
+ file_bytes, bytes,
+ "File content should match original bytes"
+ );
+
+ // Re-read SheetData from file bytes
+ let restored_from_file =
+ read_sheet_data(&file_bytes).expect("Failed to read from file bytes");
-/// Test file system read/write
-#[test]
-fn test_file_system_roundtrip() {
- // Create test data
- let mapping1 = LocalMapping::new(
- vec!["file0.txt".to_string()],
- IndexSource::new(1, 1),
- LocalMappingForward::Latest,
- )
- .unwrap();
-
- let mapping2 = LocalMapping::new(
- vec!["dir1".to_string(), "file1.txt".to_string()],
- IndexSource::new(2, 2),
- LocalMappingForward::Ref {
- sheet_name: "other".to_string(),
- },
- )
- .unwrap();
-
- let mapping3 = LocalMapping::new(
- vec!["dir2".to_string(), "file2.txt".to_string()],
- IndexSource::new(3, 3),
- LocalMappingForward::Version { version: 35 },
- )
- .unwrap();
-
- let mut mappings = HashSet::new();
- mappings.insert(mapping1.clone());
- mappings.insert(mapping2.clone());
- mappings.insert(mapping3.clone());
-
- let sheet_data = SheetData { mappings };
-
- // Convert to bytes
- let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
-
- // Write to file
- let test_file_path = ".temp/test.sheet";
- let test_file_path_hex = ".temp/test_hex.txt";
-
- // Ensure directory exists
- if let Some(parent) = std::path::Path::new(test_file_path).parent() {
- fs::create_dir_all(parent).expect("Failed to create test directory");
+ // Use SheetData's Eq trait for direct comparison
+ assert_eq!(
+ restored_from_file, sheet_data,
+ "Restored sheet data should be equal to original"
+ );
+
+ // Verify mappings in SheetData read from file
+ // Check if each original mapping can be found in restored data
+ for original_mapping in &sheet_data.mappings {
+ let found = restored_from_file
+ .mappings
+ .iter()
+ .any(|m| m == original_mapping);
+ assert!(
+ found,
+ "Original mapping {:?} should be present in restored sheet data",
+ original_mapping
+ );
+ }
+
+ // Also check if each mapping in restored data can be found in original data
+ for restored_mapping in &restored_from_file.mappings {
+ let found = sheet_data.mappings.iter().any(|m| m == restored_mapping);
+ assert!(
+ found,
+ "Restored mapping {:?} should be present in original sheet data",
+ restored_mapping
+ );
+ }
+
+ // Test file remains in .temp/test.sheet for subsequent inspection
+ // Note: Need to manually clean up .temp directory before next test run
}
- fs::write(test_file_path, &bytes).expect("Failed to write test file");
- fs::write(test_file_path_hex, hex_display_slice(&bytes)).expect("Failed to write test file");
+ /// Test reading and writing different forward types
+ #[test]
+ fn test_different_forward_types() {
+ // Test Latest type
+ let mapping_latest = LocalMapping::new(
+ vec!["latest.txt".to_string()],
+ IndexSource::new_local(1, 1),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ // Test Ref type
+ let mapping_ref = LocalMapping::new(
+ vec!["ref.txt".to_string()],
+ IndexSource::new_local(2, 2),
+ LocalMappingForward::Ref {
+ sheet_name: "reference_sheet".to_string(),
+ },
+ )
+ .unwrap();
+
+ // Test Version type
+ let mapping_version = LocalMapping::new(
+ vec!["version.txt".to_string()],
+ IndexSource::new_local(3, 3),
+ LocalMappingForward::Version { version: 54321 },
+ )
+ .unwrap();
- // Read file
- let file_bytes = fs::read(test_file_path).expect("Failed to read test file");
+ let mut mappings = HashSet::new();
+ mappings.insert(mapping_latest.clone());
+ mappings.insert(mapping_ref.clone());
+ mappings.insert(mapping_version.clone());
- // Verify file content matches original bytes
- assert_eq!(
- file_bytes, bytes,
- "File content should match original bytes"
- );
+ let sheet_data = SheetData { mappings };
- // Re-read SheetData from file bytes
- let restored_from_file = read_sheet_data(&file_bytes).expect("Failed to read from file bytes");
+ // Convert to bytes and re-read
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+ let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
- // Use SheetData's Eq trait for direct comparison
- assert_eq!(
- restored_from_file, sheet_data,
- "Restored sheet data should be equal to original"
- );
+ // Verify all mappings exist
+ assert_eq!(restored_sheet_data.mappings.len(), 3);
- // Verify mappings in SheetData read from file
- // Check if each original mapping can be found in restored data
- for original_mapping in &sheet_data.mappings {
- let found = restored_from_file
+ // Verify Latest type
+ let restored_latest = restored_sheet_data
.mappings
- .iter()
- .any(|m| m == original_mapping);
- assert!(
- found,
- "Original mapping {:?} should be present in restored sheet data",
- original_mapping
- );
+ .get(&vec!["latest.txt".to_string()])
+ .unwrap();
+ let (latest_type, latest_len, _) = restored_latest.forward().unpack();
+ assert_eq!(latest_type, 0);
+ assert_eq!(latest_len, 0);
+
+ // Verify Ref type
+ let restored_ref = restored_sheet_data
+ .mappings
+ .get(&vec!["ref.txt".to_string()])
+ .unwrap();
+ let (ref_type, ref_len, ref_bytes) = restored_ref.forward().unpack();
+ assert_eq!(ref_type, 1);
+ assert_eq!(ref_len as usize, "reference_sheet".len());
+ assert_eq!(String::from_utf8(ref_bytes).unwrap(), "reference_sheet");
+
+ // Verify Version type
+ let restored_version = restored_sheet_data
+ .mappings
+ .get(&vec!["version.txt".to_string()])
+ .unwrap();
+ let (version_type, version_len, version_bytes) = restored_version.forward().unpack();
+ assert_eq!(version_type, 2);
+ assert_eq!(version_len, 2); // u16 is 2 bytes
+ assert_eq!(u16::from_be_bytes(version_bytes.try_into().unwrap()), 54321);
}
- // Also check if each mapping in restored data can be found in original data
- for restored_mapping in &restored_from_file.mappings {
- let found = sheet_data.mappings.iter().any(|m| m == restored_mapping);
- assert!(
- found,
- "Restored mapping {:?} should be present in original sheet data",
- restored_mapping
- );
+ /// Test duplicate index source optimization
+ #[test]
+ fn test_duplicate_index_source_optimization() {
+ // Create multiple mappings sharing the same index source
+ let shared_source = IndexSource::new_local(777, 88);
+
+ let mapping1 = LocalMapping::new(
+ vec!["file1.txt".to_string()],
+ shared_source,
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mapping2 = LocalMapping::new(
+ vec!["file2.txt".to_string()],
+ shared_source,
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mapping3 = LocalMapping::new(
+ vec!["file3.txt".to_string()],
+ shared_source,
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mut mappings = HashSet::new();
+ mappings.insert(mapping1);
+ mappings.insert(mapping2);
+ mappings.insert(mapping3);
+
+ let sheet_data = SheetData { mappings };
+
+ // Convert to bytes
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+
+ // Verify index table should have only one entry
+ let index_count = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]);
+ assert_eq!(index_count, 1, "Should have only one unique index source");
+
+ // Re-read and verify
+ let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
+ assert_eq!(restored_sheet_data.mappings.len(), 3);
+
+ // Verify all mappings use the same index source
+ for mapping in &restored_sheet_data.mappings {
+ assert_eq!(mapping.index_source().id(), 777);
+ assert_eq!(mapping.index_source().version(), 88);
+ }
}
- // Test file remains in .temp/test.sheet for subsequent inspection
- // Note: Need to manually clean up .temp directory before next test run
-}
+ /// Test path serialization and deserialization
+ #[test]
+ fn test_path_serialization_deserialization() {
+ // Test various paths
+ let test_cases = vec![
+ vec!["single".to_string()],
+ vec!["dir".to_string(), "file.txt".to_string()],
+ vec![
+ "a".to_string(),
+ "b".to_string(),
+ "c".to_string(),
+ "d.txt".to_string(),
+ ],
+ vec!["with spaces".to_string(), "file name.txt".to_string()],
+ vec!["unicode".to_string(), "文件.txt".to_string()],
+ ];
+
+ for path in test_cases {
+ let mapping = LocalMapping::new(
+ path.clone(),
+ IndexSource::new_local(1, 1),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mut mappings = HashSet::new();
+ mappings.insert(mapping);
+
+ let sheet_data = SheetData { mappings };
+
+ // Convert to bytes and re-read
+ let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+ let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
+
+ // Verify path
+ let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap();
+ assert_eq!(
+ restored_mapping.value(),
+ &path,
+ "Path should be preserved after roundtrip"
+ );
+ }
+ }
-/// Test reading and writing different forward types
-#[test]
-fn test_different_forward_types() {
- // Test Latest type
- let mapping_latest = LocalMapping::new(
- vec!["latest.txt".to_string()],
- IndexSource::new(1, 1),
- LocalMappingForward::Latest,
- )
- .unwrap();
-
- // Test Ref type
- let mapping_ref = LocalMapping::new(
- vec!["ref.txt".to_string()],
- IndexSource::new(2, 2),
- LocalMappingForward::Ref {
- sheet_name: "reference_sheet".to_string(),
- },
- )
- .unwrap();
-
- // Test Version type
- let mapping_version = LocalMapping::new(
- vec!["version.txt".to_string()],
- IndexSource::new(3, 3),
- LocalMappingForward::Version { version: 54321 },
- )
- .unwrap();
-
- let mut mappings = HashSet::new();
- mappings.insert(mapping_latest.clone());
- mappings.insert(mapping_ref.clone());
- mappings.insert(mapping_version.clone());
-
- let sheet_data = SheetData { mappings };
-
- // Convert to bytes and re-read
- let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
- let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
-
- // Verify all mappings exist
- assert_eq!(restored_sheet_data.mappings.len(), 3);
-
- // Verify Latest type
- let restored_latest = restored_sheet_data
- .mappings
- .get(&vec!["latest.txt".to_string()])
+ /// Test mixed local and remote index sources
+ #[test]
+ fn test_mixed_local_remote_index_sources() {
+ // Create mappings with mixed local and remote index sources
+ let mapping_local1 = LocalMapping::new(
+ vec!["local1.txt".to_string()],
+ IndexSource::new_local(100, 1),
+ LocalMappingForward::Latest,
+ )
.unwrap();
- let (latest_type, latest_len, _) = restored_latest.forward().unpack();
- assert_eq!(latest_type, 0);
- assert_eq!(latest_len, 0);
-
- // Verify Ref type
- let restored_ref = restored_sheet_data
- .mappings
- .get(&vec!["ref.txt".to_string()])
+
+ let mapping_local2 = LocalMapping::new(
+ vec!["local2.txt".to_string()],
+ IndexSource::new_local(200, 2),
+ LocalMappingForward::Ref {
+ sheet_name: "ref_sheet".to_string(),
+ },
+ )
.unwrap();
- let (ref_type, ref_len, ref_bytes) = restored_ref.forward().unpack();
- assert_eq!(ref_type, 1);
- assert_eq!(ref_len as usize, "reference_sheet".len());
- assert_eq!(String::from_utf8(ref_bytes).unwrap(), "reference_sheet");
-
- // Verify Version type
- let restored_version = restored_sheet_data
- .mappings
- .get(&vec!["version.txt".to_string()])
+
+ let mapping_remote1 = LocalMapping::new(
+ vec!["remote1.txt".to_string()],
+ IndexSource::new_remote(300, 3),
+ LocalMappingForward::Latest,
+ )
.unwrap();
- let (version_type, version_len, version_bytes) = restored_version.forward().unpack();
- assert_eq!(version_type, 2);
- assert_eq!(version_len, 2); // u16 is 2 bytes
- assert_eq!(u16::from_be_bytes(version_bytes.try_into().unwrap()), 54321);
-}
-/// Test duplicate index source optimization
-#[test]
-fn test_duplicate_index_source_optimization() {
- // Create multiple mappings sharing the same index source
- let shared_source = IndexSource::new(777, 88);
-
- let mapping1 = LocalMapping::new(
- vec!["file1.txt".to_string()],
- shared_source,
- LocalMappingForward::Latest,
- )
- .unwrap();
-
- let mapping2 = LocalMapping::new(
- vec!["file2.txt".to_string()],
- shared_source,
- LocalMappingForward::Latest,
- )
- .unwrap();
-
- let mapping3 = LocalMapping::new(
- vec!["file3.txt".to_string()],
- shared_source,
- LocalMappingForward::Latest,
- )
- .unwrap();
-
- let mut mappings = HashSet::new();
- mappings.insert(mapping1);
- mappings.insert(mapping2);
- mappings.insert(mapping3);
-
- let sheet_data = SheetData { mappings };
-
- // Convert to bytes
- let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
-
- // Verify index table should have only one entry
- let index_count = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]);
- assert_eq!(index_count, 1, "Should have only one unique index source");
-
- // Re-read and verify
- let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
- assert_eq!(restored_sheet_data.mappings.len(), 3);
-
- // Verify all mappings use the same index source
- for mapping in &restored_sheet_data.mappings {
- assert_eq!(mapping.index_source().id(), 777);
- assert_eq!(mapping.index_source().version(), 88);
- }
-}
+ let mapping_remote2 = LocalMapping::new(
+ vec!["remote2.txt".to_string()],
+ IndexSource::new_remote(400, 4),
+ LocalMappingForward::Version { version: 12345 },
+ )
+ .unwrap();
-/// Test path serialization and deserialization
-#[test]
-fn test_path_serialization_deserialization() {
- // Test various paths
- let test_cases = vec![
- vec!["single".to_string()],
- vec!["dir".to_string(), "file.txt".to_string()],
- vec![
- "a".to_string(),
- "b".to_string(),
- "c".to_string(),
- "d.txt".to_string(),
- ],
- vec!["with spaces".to_string(), "file name.txt".to_string()],
- vec!["unicode".to_string(), "文件.txt".to_string()],
- ];
-
- for path in test_cases {
- let mapping = LocalMapping::new(
- path.clone(),
- IndexSource::new(1, 1),
+ // Test same ID but different remote status
+ let mapping_same_id_local = LocalMapping::new(
+ vec!["same_id_local.txt".to_string()],
+ IndexSource::new_local(500, 5),
+ LocalMappingForward::Latest,
+ )
+ .unwrap();
+
+ let mapping_same_id_remote = LocalMapping::new(
+ vec!["same_id_remote.txt".to_string()],
+ IndexSource::new_remote(500, 5),
LocalMappingForward::Latest,
)
.unwrap();
let mut mappings = HashSet::new();
- mappings.insert(mapping);
+ mappings.insert(mapping_local1.clone());
+ mappings.insert(mapping_local2.clone());
+ mappings.insert(mapping_remote1.clone());
+ mappings.insert(mapping_remote2.clone());
+ mappings.insert(mapping_same_id_local.clone());
+ mappings.insert(mapping_same_id_remote.clone());
let sheet_data = SheetData { mappings };
- // Convert to bytes and re-read
+ // Convert to bytes
let bytes = convert_sheet_data_to_bytes(sheet_data.clone());
+
+ // Re-read from bytes
let restored_sheet_data = read_sheet_data(&bytes).expect("Failed to read sheet data");
- // Verify path
- let restored_mapping = restored_sheet_data.mappings.iter().next().unwrap();
+ // Verify all mappings exist
+ assert_eq!(
+ restored_sheet_data.mappings.len(),
+ 6,
+ "Should have all 6 mappings"
+ );
+
+ // Verify local mappings
+ let restored_local1 = restored_sheet_data
+ .mappings
+ .get(&vec!["local1.txt".to_string()])
+ .unwrap();
+ assert_eq!(restored_local1.index_source().id(), 100);
+ assert_eq!(restored_local1.index_source().version(), 1);
+ assert_eq!(restored_local1.index_source().is_remote(), false);
+
+ let restored_local2 = restored_sheet_data
+ .mappings
+ .get(&vec!["local2.txt".to_string()])
+ .unwrap();
+ assert_eq!(restored_local2.index_source().id(), 200);
+ assert_eq!(restored_local2.index_source().version(), 2);
+ assert_eq!(restored_local2.index_source().is_remote(), false);
+
+ // Verify remote mappings
+ let restored_remote1 = restored_sheet_data
+ .mappings
+ .get(&vec!["remote1.txt".to_string()])
+ .unwrap();
+ assert_eq!(restored_remote1.index_source().id(), 300);
+ assert_eq!(restored_remote1.index_source().version(), 3);
+ assert_eq!(restored_remote1.index_source().is_remote(), true);
+
+ let restored_remote2 = restored_sheet_data
+ .mappings
+ .get(&vec!["remote2.txt".to_string()])
+ .unwrap();
+ assert_eq!(restored_remote2.index_source().id(), 400);
+ assert_eq!(restored_remote2.index_source().version(), 4);
+ assert_eq!(restored_remote2.index_source().is_remote(), true);
+
+ // Verify same ID but different remote status are treated as different sources
+ let restored_same_id_local = restored_sheet_data
+ .mappings
+ .get(&vec!["same_id_local.txt".to_string()])
+ .unwrap();
+ assert_eq!(restored_same_id_local.index_source().id(), 500);
+ assert_eq!(restored_same_id_local.index_source().version(), 5);
+ assert_eq!(restored_same_id_local.index_source().is_remote(), false);
+
+ let restored_same_id_remote = restored_sheet_data
+ .mappings
+ .get(&vec!["same_id_remote.txt".to_string()])
+ .unwrap();
+ assert_eq!(restored_same_id_remote.index_source().id(), 500);
+ assert_eq!(restored_same_id_remote.index_source().version(), 5);
+ assert_eq!(restored_same_id_remote.index_source().is_remote(), true);
+
+ // Verify that local and remote with same ID are different
+ assert_ne!(
+ restored_same_id_local.index_source(),
+ restored_same_id_remote.index_source()
+ );
+
+ // Verify forward types are preserved
+ let (forward_type_local2, forward_len_local2, forward_bytes_local2) =
+ restored_local2.forward().unpack();
+ assert_eq!(forward_type_local2, 1); // Ref type
+ assert_eq!(forward_len_local2 as usize, "ref_sheet".len());
+ assert_eq!(
+ String::from_utf8(forward_bytes_local2).unwrap(),
+ "ref_sheet"
+ );
+
+ let (forward_type_remote2, forward_len_remote2, forward_bytes_remote2) =
+ restored_remote2.forward().unpack();
+ assert_eq!(forward_type_remote2, 2); // Version type
+ assert_eq!(forward_len_remote2, 2); // u16 is 2 bytes
assert_eq!(
- restored_mapping.value(),
- &path,
- "Path should be preserved after roundtrip"
+ u16::from_be_bytes(forward_bytes_remote2.try_into().unwrap()),
+ 12345
);
+
+ // Test duplicate index source optimization with remote flag
+ // Should have 6 unique index sources (local1, local2, remote1, remote2, local500, remote500)
+ let index_count = u32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]);
+ assert_eq!(
+ index_count, 6,
+ "Should have 6 unique index sources (including remote flag)"
+ );
+
+ println!("Mixed local/remote test passed successfully!");
}
}
diff --git a/systems/sheet/src/sheet/v1/writer.rs b/systems/sheet/src/sheet/v1/writer.rs
index 00f0987..e310029 100644
--- a/systems/sheet/src/sheet/v1/writer.rs
+++ b/systems/sheet/src/sheet/v1/writer.rs
@@ -17,11 +17,15 @@ pub fn convert_sheet_data_to_bytes(sheet_data: SheetData) -> Vec<u8> {
for mapping in &mappings {
let source = mapping.index_source();
- let key = (source.id(), source.version());
+ let key = (source.is_remote(), source.id(), source.version());
if !source_to_offset.contains_key(&key) {
let offset = index_sources.len() as u32;
source_to_offset.insert(key, offset);
- index_sources.push(IndexSource::new(source.id(), source.version()));
+ index_sources.push(IndexSource::new(
+ source.is_remote(),
+ source.id(),
+ source.version(),
+ ));
}
}
@@ -86,6 +90,8 @@ pub fn convert_sheet_data_to_bytes(sheet_data: SheetData) -> Vec<u8> {
for source in &index_sources {
result.extend_from_slice(&source.id().to_le_bytes()); // Index ID (4 bytes)
result.extend_from_slice(&source.version().to_le_bytes()); // Index version (2 bytes)
+ result.push(if source.is_remote() { 1 } else { 0 }); // Remote flag (1 byte)
+ result.extend_from_slice(&[0u8; 3]); // Reserved bytes (3 bytes)
}
// 8. Bucket data
@@ -110,7 +116,7 @@ pub fn calculate_path_hash(path: &[String]) -> u32 {
fn write_mapping_bucket(
result: &mut Vec<u8>,
mapping: &LocalMapping,
- source_to_offset: &HashMap<(u32, u16), u32>,
+ source_to_offset: &HashMap<(bool, u32, u16), u32>,
) {
// Serialize path
let path_bytes = serialize_path(mapping.value());
@@ -121,7 +127,7 @@ fn write_mapping_bucket(
// Get index offset
let source = mapping.index_source();
- let key = (source.id(), source.version());
+ let key = (source.is_remote(), source.id(), source.version());
let index_offset = source_to_offset.get(&key).unwrap();
// Write mapping bucket entry
@@ -196,7 +202,7 @@ mod tests {
fn test_calculate_mapping_bucket_size() {
let mapping = LocalMapping::new(
vec!["test".to_string(), "file.txt".to_string()],
- IndexSource::new(1, 1),
+ IndexSource::new_local(1, 1),
LocalMappingForward::Latest,
)
.unwrap();
@@ -228,7 +234,7 @@ mod tests {
let mut sheet_data = SheetData::empty();
let mapping = LocalMapping::new(
vec!["dir".to_string(), "file.txt".to_string()],
- IndexSource::new(1, 1),
+ IndexSource::new_local(1, 1),
LocalMappingForward::Latest,
)
.unwrap();