use quick_xml::{Reader, Writer, events::Event};
use std::io::{Cursor, Write};
use crate::errors::XlsxError;
pub(crate) fn validate_xlsx_format(file_data: &[u8]) -> Result<(), XlsxError> {
if file_data.len() < 22 {
return Err(XlsxError::InvalidZipFormat);
}
let signature = u32::from_le_bytes([
file_data[0], file_data[1], file_data[2], file_data[3]
]);
match signature {
0x04034b50 | 0x06054b50 | 0x08074b50 => {
},
_ => return Err(XlsxError::InvalidZipFormat),
}
Ok(())
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum EachType {
Row, Col, None, }
#[derive(Debug, Clone)]
pub(crate) struct EachBlockInfo {
each_type: EachType,
var_name: String, start_row: Option<u32>, end_row: Option<u32>, start_col: Option<u32>, end_col: Option<u32>, }
pub(crate) fn merge_handlebars_in_xml(xml_content: String) -> Result<String, Box<dyn std::error::Error>> {
if !xml_content.contains('{') {
return Ok(xml_content);
}
let mut each_block_stack = Vec::<EachBlockInfo>::new();
let mut reader = Reader::from_str(&xml_content);
let mut writer = Writer::new(Cursor::new(Vec::new()));
let mut buf = Vec::new();
let mut text_buffer = String::new();
let mut brace_count = 0; let mut in_handlebars = false;
let mut _is_in_row = false;
let mut _is_in_c = false;
let mut current_row: u32 = 0;
let mut current_col: u32 = 0;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Text(ref e)) => {
let text = std::str::from_utf8(e)?;
for ch in text.chars() {
if ch == '{' {
brace_count += 1;
if brace_count >= 2 {
in_handlebars = true;
}
} else if ch == '}' {
if brace_count > 0 {
brace_count -= 1;
}
if brace_count == 0 {
in_handlebars = false;
}
}
}
text_buffer.push_str(text);
if !in_handlebars && brace_count == 0 && !text_buffer.is_empty() {
if text_buffer.contains("{{#each") {
let mut remaining = text_buffer.as_str();
while let Some(start_idx) = remaining.find("{{#each ") {
let after_each = &remaining[start_idx + 8..]; let var_name = after_each
.split('}')
.next()
.unwrap_or("")
.trim()
.to_string();
each_block_stack.push(EachBlockInfo {
each_type: EachType::None, var_name,
start_row: Some(current_row), end_row: None,
start_col: Some(current_col), end_col: None,
});
remaining = &after_each[1..];
}
}
if text_buffer.contains("{{/each}}") {
let count = text_buffer.matches("{{/each}}").count();
for _ in 0..count {
if let Some(mut block_info) = each_block_stack.pop() {
block_info.end_row = Some(current_row);
block_info.end_col = Some(current_col);
let _var_name = block_info.var_name;
let row_offset_per_item = if let (Some(start), Some(end)) = (block_info.start_row, block_info.end_row) {
end.saturating_sub(start)
} else {
0
};
let col_offset_per_item = if let (Some(start), Some(end)) = (block_info.start_col, block_info.end_col) {
end.saturating_sub(start)
} else {
0
};
if block_info.each_type == EachType::Row {
text_buffer = format!("{{{{row_offset_plus {row_offset_per_item}}}}}{text_buffer}");
} else if block_info.each_type == EachType::Col {
text_buffer = format!("{{{{col_offset_plus {col_offset_per_item}}}}}{text_buffer}");
}
} else {
break;
}
}
}
writer.write_event(Event::Text(quick_xml::events::BytesText::new(&text_buffer)))?;
text_buffer.clear();
}
}
Ok(Event::Start(ref e)) => {
if !in_handlebars && brace_count == 0 {
if !text_buffer.is_empty() {
writer.write_event(Event::Text(quick_xml::events::BytesText::new(&text_buffer)))?;
text_buffer.clear();
}
let tag_name = e.name().as_ref().to_vec();
if tag_name == b"row" {
_is_in_row = true;
for attr in e.attributes().flatten() {
let key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
if key == "r" {
let value = std::str::from_utf8(&attr.value).unwrap_or("0");
current_row = value.parse::<u32>().unwrap_or(0);
break;
}
}
if let Some(last) = each_block_stack.last_mut()
&& (last.each_type == EachType::None || last.each_type == EachType::Col) {
last.each_type = EachType::Row;
}
let mut new_start = e.borrow();
new_start.clear_attributes();
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())?;
if key == "r" {
let value = std::str::from_utf8(&attr.value)?;
let row_num = value.parse::<u32>().unwrap_or(0);
let value = format!("{{{{col_offset_reset}}}}{{{{set_row_inline {row_num}}}}}{{{{_r}}}}");
new_start.push_attribute((key.as_bytes(), value.as_bytes()));
} else {
new_start.push_attribute((attr.key.as_ref(), attr.value.as_ref()));
}
}
writer.write_event(Event::Start(new_start))?;
} else if tag_name == b"c" {
_is_in_c = true;
for attr in e.attributes().flatten() {
let key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
if key == "r" {
let value = std::str::from_utf8(&attr.value).unwrap_or("");
let r_char: String = value.chars().take_while(|c| c.is_alphabetic()).collect();
current_col = excel_column_index(&r_char);
break;
}
}
if let Some(last) = each_block_stack.last_mut()
&& last.each_type == EachType::None {
last.each_type = EachType::Col;
}
let mut new_start = e.borrow();
new_start.clear_attributes();
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())?;
if key == "r" {
let value = std::str::from_utf8(&attr.value)?;
let r_char: String = value.chars().take_while(|c| c.is_alphabetic()).collect();
let col_inline = excel_column_index(&r_char);
let value = format!("{{{{set_col_inline {col_inline}}}}}{{{{_cr}}}}");
new_start.push_attribute((key.as_bytes(), value.as_bytes()));
} else {
new_start.push_attribute((attr.key.as_ref(), attr.value.as_ref()));
}
}
writer.write_event(Event::Start(new_start))?;
} else {
writer.write_event(Event::Start(e.clone()))?;
}
}
}
Ok(Event::End(ref e)) => {
if !in_handlebars && brace_count == 0 {
if !text_buffer.is_empty() {
writer.write_event(Event::Text(quick_xml::events::BytesText::new(&text_buffer)))?;
text_buffer.clear();
}
let tag_name = e.name().as_ref().to_vec();
if tag_name == b"row" {
_is_in_row = false;
} else if tag_name == b"c" {
_is_in_c = false;
}
writer.write_event(Event::End(e.clone()))?;
}
}
Ok(Event::Empty(ref e)) => {
if !in_handlebars && brace_count == 0 {
if !text_buffer.is_empty() {
writer.write_event(Event::Text(quick_xml::events::BytesText::new(&text_buffer)))?;
text_buffer.clear();
}
writer.write_event(Event::Empty(e.clone()))?;
}
}
Ok(Event::Eof) => break,
Ok(event) => {
if !in_handlebars && brace_count == 0 {
if !text_buffer.is_empty() {
writer.write_event(Event::Text(quick_xml::events::BytesText::new(&text_buffer)))?;
text_buffer.clear();
}
writer.write_event(event)?;
}
}
Err(e) => return Err(format!("XML解析错误 at position {}: {:?}", reader.buffer_position(), e).into()),
}
buf.clear();
}
if !text_buffer.is_empty() {
writer.write_event(Event::Text(quick_xml::events::BytesText::new(&text_buffer)))?;
}
let result = writer.into_inner().into_inner();
Ok(String::from_utf8(result)?)
}
pub(crate) fn replace_shared_strings_in_sheet(
sheet_xml: &str,
shared_strings: &[String]
) -> Result<String, Box<dyn std::error::Error>> {
let mut reader = Reader::from_str(sheet_xml);
let mut writer = Writer::new(Cursor::new(Vec::new()));
let mut buf = Vec::new();
let mut in_shared_string_cell = false; let mut current_cell_attrs: Vec<(Vec<u8>, Vec<u8>)> = Vec::new(); let mut shared_string_v_content = String::new();
let mut in_cell = false; let mut found_f_tag = false; let mut in_v_tag = false;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let tag_name = e.name().as_ref().to_vec();
if tag_name == b"c" {
in_cell = true;
found_f_tag = false;
let mut has_shared_string = false;
let mut attrs = Vec::new();
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())?;
let value = std::str::from_utf8(&attr.value)?;
if key == "t" && value == "s" {
has_shared_string = true;
attrs.push((attr.key.as_ref().to_vec(), b"inlineStr".to_vec()));
} else {
attrs.push((attr.key.as_ref().to_vec(), attr.value.to_vec()));
}
}
if has_shared_string {
in_shared_string_cell = true;
current_cell_attrs = attrs;
} else {
writer.write_event(Event::Start(e.clone()))?;
}
} else if tag_name == b"f" && in_cell {
found_f_tag = true;
writer.write_event(Event::Start(e.clone()))?;
} else if tag_name == b"v" && in_cell {
in_v_tag = true;
if in_shared_string_cell {
if found_f_tag {
} else {
shared_string_v_content.clear();
}
} else if found_f_tag {
} else {
writer.write_event(Event::Start(e.clone()))?;
}
} else if in_shared_string_cell {
writer.write_event(Event::Start(e.clone()))?;
} else {
writer.write_event(Event::Start(e.clone()))?;
}
}
Ok(Event::End(ref e)) => {
let tag_name = e.name().as_ref().to_vec();
if tag_name == b"c" {
if in_shared_string_cell {
let mut new_start = quick_xml::events::BytesStart::new("c");
for (key, value) in ¤t_cell_attrs {
new_start.push_attribute((key.as_slice(), value.as_slice()));
}
writer.write_event(Event::Start(new_start))?;
if !shared_string_v_content.is_empty() && !found_f_tag
&& let Ok(index) = shared_string_v_content.parse::<usize>()
&& index < shared_strings.len() {
let si_content = &shared_strings[index];
let si_content = replace_shared_string_si_with_handlebars(si_content)?;
let mut is_reader = Reader::from_str(&si_content);
let mut is_buf = Vec::new();
loop {
match is_reader.read_event_into(&mut is_buf) {
Ok(Event::Eof) => break,
Ok(ev) => writer.write_event(ev)?,
Err(e) => return Err(format!("shared_string parse error: {:?}", e).into()),
}
is_buf.clear();
}
}
writer.write_event(Event::End(e.clone()))?;
in_shared_string_cell = false;
current_cell_attrs.clear();
shared_string_v_content.clear();
} else {
writer.write_event(Event::End(e.clone()))?;
}
in_cell = false;
found_f_tag = false;
} else if tag_name == b"f" && in_cell {
writer.write_event(Event::End(e.clone()))?;
} else if tag_name == b"v" && in_v_tag {
in_v_tag = false;
if in_shared_string_cell {
if found_f_tag {
}
} else if found_f_tag {
} else {
writer.write_event(Event::End(e.clone()))?;
}
} else if in_shared_string_cell {
writer.write_event(Event::End(e.clone()))?;
} else {
writer.write_event(Event::End(e.clone()))?;
}
}
Ok(Event::Text(ref e)) => {
if in_v_tag && found_f_tag {
} else if in_v_tag && in_shared_string_cell {
let text = std::str::from_utf8(e)?;
shared_string_v_content.push_str(text);
} else {
writer.write_event(Event::Text(e.clone()))?;
}
}
Ok(Event::Empty(ref e)) => {
if !in_shared_string_cell {
writer.write_event(Event::Empty(e.clone()))?;
}
}
Ok(Event::Eof) => break,
Ok(event) => {
if !in_shared_string_cell {
writer.write_event(event)?;
}
}
Err(e) => return Err(format!("XML解析错误 at position {}: {:?}", reader.buffer_position(), e).into()),
}
buf.clear();
}
let result = writer.into_inner().into_inner();
Ok(String::from_utf8(result)?)
}
pub(crate) fn replace_shared_string_si_with_handlebars(
si_xml: &str
) -> Result<String, Box<dyn std::error::Error>> {
let mut reader = Reader::from_str(si_xml);
let mut writer = Writer::new(Cursor::new(Vec::new()));
let mut in_r_tag = false; let mut in_t_tag = false; let mut t_text_content = String::new(); let mut t_events: Vec<Event<'_>> = Vec::new();
loop {
match reader.read_event() {
Ok(Event::Start(ref e)) => {
let tag_name = e.name().as_ref().to_vec();
if tag_name == b"r" {
in_r_tag = true;
writer.write_event(Event::Start(e.clone()))?;
} else if tag_name == b"t" {
in_t_tag = true;
t_text_content.clear();
t_events.clear();
} else if in_t_tag {
t_events.push(Event::Start(e.clone()));
} else {
writer.write_event(Event::Start(e.clone()))?;
}
}
Ok(Event::End(ref e)) => {
let tag_name = e.name().as_ref().to_vec();
if tag_name == b"t" {
in_t_tag = false;
let contains_each_start = t_text_content.contains("{{#each");
let contains_each_end = t_text_content.contains("{{/each");
if (contains_each_start || contains_each_end) && !in_r_tag {
writer.write_event(Event::Start(quick_xml::events::BytesStart::new("r")))?;
writer.write_event(Event::Start(quick_xml::events::BytesStart::new("t")))?;
for t_event in t_events.iter() {
writer.write_event(t_event.clone())?;
}
writer.write_event(Event::End(e.clone()))?; writer.write_event(Event::End(quick_xml::events::BytesEnd::new("r")))?; } else {
writer.write_event(Event::Start(quick_xml::events::BytesStart::new("t")))?;
for t_event in t_events.iter() {
writer.write_event(t_event.clone())?;
}
writer.write_event(Event::End(e.clone()))?;
}
} else if tag_name == b"r" {
in_r_tag = false;
writer.write_event(Event::End(e.clone()))?;
} else if in_t_tag {
t_events.push(Event::End(e.clone()));
} else {
writer.write_event(Event::End(e.clone()))?;
}
}
Ok(Event::Text(ref e)) => {
if in_t_tag {
let text = std::str::from_utf8(e)?;
t_text_content.push_str(text);
t_events.push(Event::Text(e.clone()));
} else {
writer.write_event(Event::Text(e.clone()))?;
}
}
Ok(Event::Empty(ref e)) => {
if in_t_tag {
t_events.push(Event::Empty(e.clone()));
} else {
writer.write_event(Event::Empty(e.clone()))?;
}
}
Ok(Event::Eof) => break,
Ok(event) => {
if in_t_tag {
t_events.push(event);
} else {
writer.write_event(event)?;
}
}
Err(e) => return Err(format!("XML解析错误 at position {}: {:?}", reader.buffer_position(), e).into()),
}
}
let result = writer.into_inner().into_inner();
Ok(String::from_utf8(result)?)
}
pub(crate) fn register_basic_helpers(handlebars: &mut handlebars::Handlebars) -> Result<(), Box<dyn std::error::Error>> {
use handlebars::handlebars_helper;
use serde_json::Value;
handlebars_helper!(eq: |x: Value, y: Value| x == y);
handlebars.register_helper("eq", Box::new(eq));
handlebars_helper!(ne: |x: Value, y: Value| x != y);
handlebars.register_helper("ne", Box::new(ne));
handlebars_helper!(gt: |x: i64, y: i64| x > y);
handlebars.register_helper("gt", Box::new(gt));
handlebars_helper!(lt: |x: i64, y: i64| x < y);
handlebars.register_helper("lt", Box::new(lt));
handlebars_helper!(upper: |s: String| s.to_uppercase());
handlebars.register_helper("upper", Box::new(upper));
handlebars_helper!(lower: |s: String| s.to_lowercase());
handlebars.register_helper("lower", Box::new(lower));
handlebars_helper!(len: |x: Value| {
match x {
Value::Array(arr) => arr.len(),
Value::String(s) => s.chars().count(),
Value::Object(obj) => obj.len(),
_ => 0
}
});
handlebars.register_helper("len", Box::new(len));
Ok(())
}
pub(crate) fn excel_column_name(current: &str, increment: u32) -> String {
let mut col_index = 0;
for (i, ch) in current.chars().rev().enumerate() {
let ch_val = (ch as u8 - b'A' + 1) as u32;
col_index += ch_val * 26_u32.pow(i as u32);
}
col_index += increment;
let mut new_col_name = String::new();
let mut n = col_index;
while n > 0 {
let rem = (n - 1) % 26;
new_col_name.push((b'A' + rem as u8) as char);
n = (n - 1) / 26;
}
new_col_name.chars().rev().collect()
}
pub(crate) fn excel_column_index(col_name: &str) -> u32 {
let mut col_index = 0;
for (i, ch) in col_name.chars().rev().enumerate() {
let ch_val = (ch as u8 - b'A' + 1) as u32;
col_index += ch_val * 26_u32.pow(i as u32);
}
col_index
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_excel_column_name() {
assert_eq!(excel_column_name("A", 0), "A");
assert_eq!(excel_column_name("A", 1), "B");
assert_eq!(excel_column_name("Z", 1), "AA");
assert_eq!(excel_column_name("AA", 1), "AB");
assert_eq!(excel_column_name("AZ", 1), "BA");
assert_eq!(excel_column_name("ZZ", 1), "AAA");
assert_eq!(excel_column_name("AAA", 26), "ABA");
}
#[test]
fn test_replace_shared_string_si_with_handlebars() {
let input_with_each_in_r = r#"<si>
<r>
<t>a</t>
</r>
<r>
<rPr>
<sz val="11" />
<color rgb="FFFF0000" />
</rPr>
<t>{{#each projects}}</t>
</r>
<phoneticPr fontId="1" type="noConversion" />
</si>"#;
let result = replace_shared_string_si_with_handlebars(input_with_each_in_r).unwrap();
println!("输入包含 {{#each 且在 r 标签内的结果:");
println!("{}", result);
assert!(result.contains("{{#each projects}}"));
assert!(result.contains("<r>"));
assert!(result.contains("<t>{{#each projects}}</t>"));
let input_with_each_not_in_r = r#"<si>
<t>{{#each projects}}</t>
<phoneticPr fontId="1" type="noConversion" />
</si>"#;
let result2 = replace_shared_string_si_with_handlebars(input_with_each_not_in_r).unwrap();
println!("输入包含 {{#each 且不在 r 标签内的结果:");
println!("{}", result2);
assert!(result2.contains("{{#each projects}}"));
assert!(result2.contains("<r><t>{{#each projects}}</t></r>"));
let input_with_end_each_not_in_r = r#"<si>
<t>{{/each}}</t>
</si>"#;
let result3 = replace_shared_string_si_with_handlebars(input_with_end_each_not_in_r).unwrap();
println!("输入包含 {{/each}} 且不在 r 标签内的结果:");
println!("{}", result3);
assert!(result3.contains("{{/each}}"));
assert!(result3.contains("<r><t>{{/each}}</t></r>"));
let input_normal = r#"<si>
<r>
<t>normal text</t>
</r>
<r>
<rPr>
<sz val="11" />
</rPr>
<t>{{name}}</t>
</r>
</si>"#;
let result4 = replace_shared_string_si_with_handlebars(input_normal).unwrap();
println!("输入不包含 each 的结果:");
println!("{}", result4);
assert!(result4.contains("<r>"));
assert!(result4.contains("{{name}}"));
}
#[test]
fn test_excel_column_index() {
assert_eq!(excel_column_index("A"), 1);
assert_eq!(excel_column_index("E"), 5);
assert_eq!(excel_column_index("Z"), 26);
assert_eq!(excel_column_index("AA"), 27);
assert_eq!(excel_column_index("AZ"), 52);
assert_eq!(excel_column_index("BA"), 53);
assert_eq!(excel_column_index("ZZ"), 702);
assert_eq!(excel_column_index("AAA"), 703);
}
#[test]
fn test_excel_column_name_and_index() {
let test_cases = vec![
("A", 1),
("Z", 26),
("AA", 27),
("AZ", 52),
("BA", 53),
("ZZ", 702),
("AAA", 703),
("AAB", 704),
("ABC", 731),
("ZZZ", 18278),
];
for (col_name, expected_index) in test_cases {
let index = excel_column_index(col_name);
assert_eq!(index, expected_index, "Column name to index failed for {}", col_name);
let name = excel_column_name(col_name, 0);
assert_eq!(name, col_name, "Column name identity failed for {}", col_name);
let name_plus_one = excel_column_name(col_name, 1);
let index_plus_one = excel_column_index(&name_plus_one);
assert_eq!(index_plus_one, expected_index + 1, "Column name to index failed for {} + 1", col_name);
}
}
}
pub(crate) fn remove_row_simple(
xml_content: &str,
target_uuid: &str
) -> Result<String, Box<dyn std::error::Error>> {
let mut reader = Reader::from_str(xml_content);
let mut writer = Writer::new(Cursor::new(Vec::new()));
let mut buf = Vec::new();
let mut current_row_content = String::new();
let mut in_row = false;
let mut row_depth = 0;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
if e.name().as_ref() == b"row" {
in_row = true;
row_depth += 1;
current_row_content.clear();
current_row_content.push_str(&format!("<{}", String::from_utf8_lossy(e.name().as_ref())));
for attr in e.attributes().flatten() {
current_row_content.push_str(&format!(" {}=\"{}\"",
String::from_utf8_lossy(attr.key.as_ref()),
String::from_utf8_lossy(&attr.value)));
}
current_row_content.push('>');
} else if in_row {
current_row_content.push_str(&format!("<{}", String::from_utf8_lossy(e.name().as_ref())));
for attr in e.attributes().flatten() {
current_row_content.push_str(&format!(" {}=\"{}\"",
String::from_utf8_lossy(attr.key.as_ref()),
String::from_utf8_lossy(&attr.value)));
}
current_row_content.push('>');
} else {
writer.write_event(Event::Start(e.clone()))?;
}
}
Ok(Event::End(ref e)) => {
if e.name().as_ref() == b"row" && in_row {
row_depth -= 1;
if row_depth == 0 {
current_row_content.push_str(&format!("</{}>", String::from_utf8_lossy(e.name().as_ref())));
if !current_row_content.contains(target_uuid) {
writer.get_mut().write_all(current_row_content.as_bytes())?;
}
in_row = false;
current_row_content.clear();
} else {
current_row_content.push_str(&format!("</{}>", String::from_utf8_lossy(e.name().as_ref())));
}
} else if in_row {
current_row_content.push_str(&format!("</{}>", String::from_utf8_lossy(e.name().as_ref())));
} else {
writer.write_event(Event::End(e.clone()))?;
}
}
Ok(Event::Text(ref e)) => {
if in_row {
let text = std::str::from_utf8(e)?;
current_row_content.push_str(text);
} else {
writer.write_event(Event::Text(e.clone()))?;
}
}
Ok(Event::Empty(ref e)) => {
if in_row {
current_row_content.push_str(&format!("<{}", String::from_utf8_lossy(e.name().as_ref())));
for attr in e.attributes().flatten() {
current_row_content.push_str(&format!(" {}=\"{}\"",
String::from_utf8_lossy(attr.key.as_ref()),
String::from_utf8_lossy(&attr.value)));
}
current_row_content.push_str("/>");
} else {
writer.write_event(Event::Empty(e.clone()))?;
}
}
Ok(Event::Comment(ref e)) => {
if !in_row {
writer.write_event(Event::Comment(e.clone()))?;
}
}
Ok(Event::Eof) => break,
Ok(event) => {
if !in_row {
writer.write_event(event)?;
}
}
Err(e) => return Err(format!("XML解析错误 at position {}: {:?}", reader.buffer_position(), e).into()),
}
buf.clear();
}
let result = writer.into_inner().into_inner();
Ok(String::from_utf8(result)?)
}