use std::{borrow::Cow, collections::HashMap, ops::Deref, string::FromUtf8Error};
#[macro_export]
macro_rules! cache_struct{
(
// meta data about struct
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
// meta data about field
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)?
}
) => {
#[cfg(feature = "cache")]
#[derive(serde::Deserialize,serde::Serialize)]
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta])*
$field_vis $field_name : $field_type,
)*
}
#[cfg(not(feature = "cache"))]
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta])*
$field_vis $field_name : $field_type,
)*
}
}
}
#[macro_export]
macro_rules! cache_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident { $($body:tt)* }) => {
#[derive(Debug)]
#[cfg(not(feature="cache"))]
$(#[$meta])*
$vis enum $name { $($body)* }
#[derive(Debug,serde::Deserialize,serde::Serialize)]
#[cfg(feature="cache")]
$(#[$meta])*
$vis enum $name { $($body)* }
};
($(#[$meta:meta])* enum $name:ident { $($body:tt)* }) => {
$(#[$meta])*
#[derive(Debug, Default)]
enum $name { $($body)* }
};
($(#[$meta:meta])* enum $name:ident<$T:ident> { $($body:tt)* }) => {
$(#[$meta])*
#[derive(Debug)]
enum $name<$T> { $($body)* }
};
}
#[derive(Debug)]
pub enum IError {
Io(std::io::Error),
InvalidArchive(Cow<'static, str>),
UnsupportedArchive(&'static str),
FileNotFound,
InvalidPassword,
Utf8(std::string::FromUtf8Error),
Xml(quick_xml::Error),
Encoding(quick_xml::encoding::EncodingError),
NoNav(&'static str),
Cover(String),
IncompleteEncoding,
InvalidHexChar(char),
Utf8ConversionError,
#[cfg(feature = "cache")]
Cache(String),
Unknown,
}
#[cfg(feature = "cache")]
impl From<serde_json::Error> for IError {
fn from(value: serde_json::Error) -> Self {
Self::Cache(format!("{:?}", value))
}
}
impl std::fmt::Display for IError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
IError::IncompleteEncoding => write!(f, "百分比编码不完整"),
IError::InvalidHexChar(c) => write!(f, "无效的十六进制字符: {}", c),
IError::Utf8ConversionError => write!(f, "UTF-8转换失败"),
_ => {
write!(f, "{:?}", self)
}
}
}
}
impl std::error::Error for IError {}
pub type IResult<T> = Result<T, IError>;
impl From<std::io::Error> for IError {
fn from(value: std::io::Error) -> Self {
IError::Io(value)
}
}
impl From<quick_xml::Error> for IError {
fn from(value: quick_xml::Error) -> Self {
match value {
quick_xml::Error::Io(e) => IError::Io(std::io::Error::other(e)),
_ => IError::Xml(value),
}
}
}
impl From<FromUtf8Error> for IError {
fn from(value: FromUtf8Error) -> Self {
IError::Utf8(value)
}
}
#[derive(Debug, Clone)]
pub enum ContentType {
Paragraph,
Heading(u8),
Image,
Link,
ListItem,
BlockQuote,
CodeBlock,
HorizontalRule,
Text,
Other(String),
}
#[derive(Debug, Clone)]
pub struct ContentItem {
pub content_type: ContentType,
pub text: String,
pub attributes: Vec<(String, String)>,
pub children: Vec<ContentItem>,
}
impl ContentItem {
pub fn new(content_type: ContentType) -> Self {
Self {
content_type,
text: String::new(),
attributes: Vec::new(),
children: Vec::new(),
}
}
pub fn add_attribute(&mut self, key: String, value: String) {
self.attributes.push((key, value));
}
pub fn add_child(&mut self, child: ContentItem) {
self.children.push(child);
}
pub fn add_text(&mut self, text: &str) {
self.text.push_str(text);
}
pub fn format(&self, indent: usize) -> String {
let indent_str = " ".repeat(indent);
let mut result = format!("{}[{:?}]", indent_str, self.content_type);
if !self.text.is_empty() {
result.push_str(&format!(" text: \"{}\"", self.text.trim()));
}
if !self.attributes.is_empty() {
result.push_str(" attribute: {");
for (i, (key, value)) in self.attributes.iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
result.push_str(&format!("{}: \"{}\"", key, value));
}
result.push('}');
}
result.push('\n');
for child in &self.children {
result.push_str(&child.format(indent + 1));
}
result
}
}
cache_struct! {
#[derive(Debug, Default)]
pub(crate) struct BookInfo {
pub(crate) title: String,
pub(crate) identifier: String,
pub(crate) creator: Option<String>,
pub(crate) description: Option<String>,
pub(crate) contributor: Option<String>,
pub(crate) date: Option<String>,
pub(crate) format: Option<String>,
pub(crate) publisher: Option<String>,
pub(crate) subject: Option<String>,
}
}
impl BookInfo {
pub(crate) fn append_creator(&mut self, v: &str) {
if let Some(c) = &mut self.creator {
c.push(',');
c.push_str(v);
} else {
self.creator = Some(String::from(v));
}
}
}
pub(crate) fn unescape_html(v: &str) -> String {
let mut reader = quick_xml::reader::Reader::from_str(v);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut txt = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(quick_xml::events::Event::Text(e)) => {
if let Ok(t) = e.decode() {
txt.push_str(t.deref());
}
}
Ok(quick_xml::events::Event::Eof) => {
break;
}
_ => (),
}
buf.clear();
}
txt
}
pub fn escape_xml<'a>(raw: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
quick_xml::escape::escape(raw)
}
pub struct DateTimeFormater {
timestamp: u64,
start_year: u64,
format_map: HashMap<char, fn(u64) -> String>,
timezone_offset: i16,
}
impl Default for DateTimeFormater {
fn default() -> Self {
Self::new(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|v| v.as_secs())
.unwrap_or(0),
)
}
}
impl DateTimeFormater {
pub fn custom_start(timestamp: u64, start_year: u64) -> Self {
let t: fn(u64) -> String = Self::format_year;
let t2: fn(u64) -> String = Self::format_day;
Self {
start_year,
timezone_offset: 0,
timestamp,
format_map: HashMap::from([
('Y', t),
('M', t2),
('d', t2),
('H', t2),
('m', t2),
('s', t2),
]),
}
}
pub fn new(timestamp: u64) -> Self {
Self::custom_start(timestamp, 1970)
}
pub fn with_timezone_offset(mut self, offset: i16) -> Self {
self.timezone_offset = offset;
self
}
pub fn format<T: AsRef<str>>(&self, pattern: T) -> String {
let (year, month, day, hour, min, sec) =
self.do_time_display(self.timestamp, self.start_year);
let values = HashMap::from([
('Y', year),
('M', month),
('d', day),
('H', hour),
('m', min),
('s', sec),
]);
let mut result = String::new();
let mut chars = pattern.as_ref().chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
if let Some(&next_c) = chars.peek() {
if let Some(formatter) = self.format_map.get(&next_c) {
result.push_str(&formatter(*values.get(&next_c).unwrap_or(&0)));
chars.next(); continue;
}
}
}
result.push(c);
}
result
}
pub fn default_format(&self) -> String {
self.format("%Y-%M-%dT%H:%m:%sZ")
}
fn format_year(value: u64) -> String {
format!("{:04}", value)
}
fn format_day(value: u64) -> String {
format!("{:02}", value)
}
fn do_time_display(&self, value: u64, start_year: u64) -> (u64, u64, u64, u64, u64, u64) {
let offset = self.timezone_offset * 60 * 60;
let value = if offset < 0 {
value - (-offset as u64)
} else {
value + (offset as u64)
};
let per_year_sec = 365 * 24 * 60 * 60;
let mut year = value / per_year_sec;
let last_sec = value - (year) * per_year_sec;
year += start_year;
let mut leap_year_sec = 0;
for y in start_year..year {
if Self::is_leap(y) {
leap_year_sec += 86400;
}
}
if last_sec < leap_year_sec {
year -= 1;
if Self::is_leap(year) {
leap_year_sec -= 86400;
}
}
let mut time = value - leap_year_sec - (year - start_year) * per_year_sec;
let mut day_of_year: [u64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let sec = time % 60;
time /= 60;
let min = time % 60;
time /= 60;
let hour = time % 24;
time /= 24;
if Self::is_leap(year) {
day_of_year[1] += 1;
}
let mut month = 0;
for (index, ele) in day_of_year.iter().enumerate() {
if &time < ele {
month = index + 1;
time += 1; break;
}
time -= ele;
}
(year, month as u64, time, hour, min, sec)
}
fn is_leap(year: u64) -> bool {
year % 4 == 0 && ((year % 100) != 0 || year % 400 == 0)
}
}
pub(crate) fn get_media_type(file_name: &str) -> String {
let f = file_name.to_lowercase();
let mut types = std::collections::HashMap::new();
types.insert(".gif", String::from("image/gif"));
types.insert(".jpg", String::from("image/jpeg"));
types.insert(".jpeg", String::from("image/jpeg"));
types.insert(".png", String::from("image/png"));
types.insert(".svg", String::from("image/svg+xml"));
types.insert(".webp", String::from("image/webp"));
types.insert(".mp3", String::from("audio/mpeg"));
types.insert(".mp4", String::from("audio/mp4"));
types.insert(".css", String::from("text/css"));
types.insert(".ttf", String::from("application/font-sfnt"));
types.insert(".oft", String::from("application/font-sfnt"));
types.insert(".woff", String::from("application/font-woff"));
types.insert(".woff", String::from("font/woff2"));
types.insert(".xhtml", String::from("application/xhtml+xml"));
types.insert(".js", String::from("application/javascript"));
types.insert(".opf", String::from("application/x-dtbncx+xml"));
let x: &[_] = &['.'];
if let Some(index) = f.rfind(x) {
let sub = &f[index..f.len()];
return match types.get(&sub) {
Some(t) => String::from(t),
None => String::new(),
};
};
String::new()
}
pub fn urldecode_enhanced(input: &str) -> IResult<String> {
let mut result = Vec::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'%' => {
let hex1 = chars.next().ok_or(IError::IncompleteEncoding)?;
let hex2 = chars.next().ok_or(IError::IncompleteEncoding)?;
let byte = decode_hex_byte(hex1, hex2)?;
result.push(byte);
}
'+' => {
result.push(b' ');
}
_ => {
let mut buf = [0; 4];
let encoded = ch.encode_utf8(&mut buf);
result.extend_from_slice(encoded.as_bytes());
}
}
}
String::from_utf8(result).map_err(|_| IError::Utf8ConversionError)
}
fn decode_hex_byte(c1: char, c2: char) -> IResult<u8> {
let high = hex_char_to_value(c1)?;
let low = hex_char_to_value(c2)?;
Ok((high << 4) | low)
}
fn hex_char_to_value(c: char) -> IResult<u8> {
match c {
'0'..='9' => Ok(c as u8 - b'0'),
'a'..='f' => Ok(c as u8 - b'a' + 10),
'A'..='F' => Ok(c as u8 - b'A' + 10),
_ => Err(IError::InvalidHexChar(c)),
}
}
pub fn get_css_content_url<T: AsRef<str> + ?Sized>(css: &T) -> Vec<&str> {
let mut res = Vec::new();
let line = css.as_ref().split("\n").collect::<Vec<&str>>();
for ele in line {
let mut index = 0;
let byte = ele.as_bytes();
let count = byte.len();
loop {
if index + 4 >= count {
break;
}
if &byte[index..(index + 4)] == b"url(" {
let mut start = index + 4;
let mut end = b')';
if byte[start] == b'\'' {
end = b'\'';
start += 1;
index += 1;
} else if byte[start] == b'"' {
end = b'"';
start += 1;
index += 1;
}
loop {
if start >= count {
index = start;
break;
}
let t = byte[start];
if t == end {
let u = &ele[(index + 4)..start];
res.push(u);
index = start + 1;
break;
}
start += 1;
}
} else {
index += 1;
}
}
}
res
}
#[cfg(test)]
pub(crate) mod tests {
use crate::common::{get_css_content_url, urldecode_enhanced, DateTimeFormater};
pub fn get_req_mem(url: &str) -> Vec<u8> {
get_req(url).send().unwrap().bytes().unwrap().to_vec()
}
pub fn get_req(url: &str) -> reqwest::blocking::RequestBuilder {
let mut req = reqwest::blocking::Client::builder();
if let Ok(proxy) = std::env::var("HTTPS_PROXY")
.or_else(|_e| std::env::var("https_proxy"))
.or_else(|_e| std::env::var("ALL_PROXY"))
.or_else(|_e| std::env::var("all_proxy"))
{
req = req.proxy(reqwest::Proxy::https(proxy).expect("invalid proxy env"));
}
req.build().unwrap().get(url)
}
pub fn download_epub_file(name: &str, url: &str) {
use super::IError;
use std::borrow::Cow;
if name.contains("/") {
let p = std::path::Path::new(&name);
std::fs::create_dir_all(format!("{}", p.parent().unwrap().display())).unwrap();
}
if std::fs::metadata(name).is_err() {
let mut res = get_req(url)
.send()
.map_err(|e: reqwest::Error| {
IError::InvalidArchive(Cow::from(format!("download fail {:?}", e)))
})
.and_then(|res| {
if !res.status().is_success() {
Err(IError::InvalidArchive(Cow::from(format!(
"download fail {:?}",
res.status()
))))
} else {
Ok(res)
}
})
.unwrap();
let mut out = std::fs::File::options()
.truncate(true)
.create(true)
.write(true)
.open(name)
.expect("file fail");
std::io::copy(&mut res, &mut out).unwrap();
}
}
pub fn download_zip_file(name: &str, url: &str) -> String {
use super::IError;
use std::{borrow::Cow, io::Read};
let out = if std::path::Path::new("target").exists() {
format!("target/{name}")
} else {
format!("../target/{name}")
};
if std::fs::metadata(&out).is_err() {
let zip = get_req_mem(url);
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(zip))
.map_err(|e| IError::InvalidArchive(Cow::from(format!("download fail {:?}", e))))
.expect("zip fail");
let mut zip = zip.by_name(name).unwrap();
let mut v = Vec::new();
zip.read_to_end(&mut v).unwrap();
if name.contains("/") {
std::fs::create_dir_all(std::path::Path::new(&out).parent().unwrap()).unwrap();
}
std::fs::write(std::path::Path::new(&out), &mut v).unwrap();
}
out
}
#[test]
fn test_time_format() {
assert_eq!(
"2025-07-29T11:41:46Z",
DateTimeFormater::new(1753760506)
.with_timezone_offset(8)
.default_format()
);
assert_eq!(
"2025",
DateTimeFormater::new(1753760506)
.with_timezone_offset(8)
.format("%Y")
);
assert_eq!(
"2025-07-01T22:00:00Z",
DateTimeFormater::new(1751407200).default_format()
);
assert_eq!(
"2025-07-02T06:00:00Z",
DateTimeFormater::new(1751407200)
.with_timezone_offset(8)
.default_format()
);
assert_eq!(
"2025-07-01T14:00:00Z",
DateTimeFormater::new(1751407200)
.with_timezone_offset(-8)
.default_format()
);
}
#[test]
fn decode_url() {
assert_eq!(
urldecode_enhanced("Images/c5eiR%E7%BF%BB%E8%AF%913.jpg").unwrap(),
"Images/c5eiR翻译3.jpg"
);
}
#[test]
fn test_get_css_content_url() {
let v = get_css_content_url(r##"background: url(../images/bg.jpg);"##);
assert_eq!(vec!["../images/bg.jpg"], v);
let v = get_css_content_url(r##"background: url("../images/bg.jpg");"##);
assert_eq!(vec!["../images/bg.jpg"], v);
let v = get_css_content_url(r##"background: url('../images/bg.jpg');"##);
assert_eq!(vec!["../images/bg.jpg"], v);
let v = get_css_content_url(r##"background: url('../images/bg.jpg);"##);
assert!(v.is_empty());
let v = get_css_content_url(r##"background: url('../images/bg.jpg");"##);
assert!(v.is_empty());
let v = get_css_content_url(r##"background: url("../images/bg.jpg');"##);
assert!(v.is_empty());
let v = get_css_content_url(r##"background: url('../images(/b)g.jpg');"##);
assert_eq!(vec!["../images(/b)g.jpg"], v);
let v = get_css_content_url(r##"background: url('../images(/bg.jpg');"##);
assert_eq!(vec!["../images(/bg.jpg"], v);
let v = get_css_content_url(r##"background: url("../imag(es/bg.jpg');"##);
assert!(v.is_empty());
let v = get_css_content_url(r##"background: url('../ima中文ges(/bg.jpg');"##);
assert_eq!(vec!["../ima中文ges(/bg.jpg"], v);
let v = get_css_content_url(
r##".back {
background-image: url(../Images/contents.jpg);
background-repeat:no-repeat;
background-position:top center;
background-size:cover;
}"##,
);
assert_eq!(vec!["../Images/contents.jpg"], v);
}
}