use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use super::{file_exists, file_get_async};
#[derive(Debug)]
pub enum IniReaderError {
Empty,
Duplicate,
OutOfBound,
NotExist,
NotParsed,
IoError(io::Error),
None,
}
impl std::fmt::Display for IniReaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IniReaderError::Empty => write!(f, "Empty document"),
IniReaderError::Duplicate => write!(f, "Duplicate section"),
IniReaderError::OutOfBound => write!(f, "Item exists outside of any section"),
IniReaderError::NotExist => write!(f, "Target does not exist"),
IniReaderError::NotParsed => write!(f, "Parse error"),
IniReaderError::IoError(e) => write!(f, "IO error: {}", e),
IniReaderError::None => write!(f, "No error"),
}
}
}
impl From<io::Error> for IniReaderError {
fn from(error: io::Error) -> Self {
IniReaderError::IoError(error)
}
}
pub struct IniReader {
content: HashMap<String, Vec<(String, String)>>,
parsed: bool,
current_section: String,
exclude_sections: HashSet<String>,
include_sections: HashSet<String>,
direct_save_sections: HashSet<String>,
section_order: Vec<String>,
last_error: IniReaderError,
pub store_any_line: bool,
pub allow_dup_section_titles: bool,
pub keep_empty_section: bool,
isolated_items_section: String,
pub store_isolated_line: bool,
}
impl Default for IniReader {
fn default() -> Self {
Self::new()
}
}
impl IniReader {
pub fn new() -> Self {
IniReader {
content: HashMap::new(),
parsed: false,
current_section: String::new(),
exclude_sections: HashSet::new(),
include_sections: HashSet::new(),
direct_save_sections: HashSet::new(),
section_order: Vec::new(),
last_error: IniReaderError::None,
store_any_line: false,
allow_dup_section_titles: false,
keep_empty_section: true,
isolated_items_section: String::new(),
store_isolated_line: false,
}
}
pub fn exclude_section(&mut self, section: &str) {
self.exclude_sections.insert(section.to_string());
}
pub fn include_section(&mut self, section: &str) {
self.include_sections.insert(section.to_string());
}
pub fn add_direct_save_section(&mut self, section: &str) {
self.direct_save_sections.insert(section.to_string());
}
pub fn set_isolated_items_section(&mut self, section: &str) {
self.isolated_items_section = section.to_string();
}
pub fn erase_section(&mut self) {
if self.current_section.is_empty() {
return;
}
if let Some(section_vec) = self.content.get_mut(&self.current_section) {
section_vec.clear();
}
}
pub fn erase_section_by_name(&mut self, section: &str) {
if !self.section_exist(section) {
return;
}
if let Some(section_vec) = self.content.get_mut(section) {
section_vec.clear();
}
}
fn should_ignore_section(&self, section: &str) -> bool {
let excluded = self.exclude_sections.contains(section);
let included = if self.include_sections.is_empty() {
true
} else {
self.include_sections.contains(section)
};
excluded || !included
}
fn should_direct_save(&self, section: &str) -> bool {
self.direct_save_sections.contains(section)
}
pub async fn from_file(path: &str) -> Result<Self, IniReaderError> {
let mut reader = IniReader::new();
reader.parse_file(path).await?;
Ok(reader)
}
pub fn get_last_error(&self) -> String {
self.last_error.to_string()
}
fn trim_whitespace(s: &str) -> String {
s.trim().to_string()
}
fn process_escape_char(s: &mut String) {
*s = s
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\t", "\t");
}
fn process_escape_char_reverse(s: &mut String) {
*s = s
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
}
pub fn erase_all(&mut self) {
self.content.clear();
self.section_order.clear();
self.current_section.clear();
self.parsed = false;
}
pub fn is_parsed(&self) -> bool {
self.parsed
}
pub fn parse(&mut self, content: &str) -> Result<(), IniReaderError> {
self.erase_all();
if content.is_empty() {
self.last_error = IniReaderError::Empty;
return Err(IniReaderError::Empty);
}
let content = if content.starts_with("\u{FEFF}") {
&content[3..]
} else {
content
};
let mut in_excluded_section = false;
let mut in_direct_save_section = false;
let mut in_isolated_section = false;
let mut cur_section = String::new();
let mut item_group = Vec::new();
let mut read_sections = Vec::new();
if self.store_isolated_line && !self.isolated_items_section.is_empty() {
cur_section = self.isolated_items_section.clone();
in_excluded_section = self.should_ignore_section(&cur_section);
in_direct_save_section = self.should_direct_save(&cur_section);
in_isolated_section = true;
}
for line in content.lines() {
let line = Self::trim_whitespace(line);
if line.is_empty()
|| line.starts_with(';')
|| line.starts_with('#')
|| line.starts_with("//")
{
continue;
}
let mut line = line.to_string();
Self::process_escape_char(&mut line);
if line.starts_with('[') && line.ends_with(']') && line.len() >= 3 {
let this_section = line[1..line.len() - 1].to_string();
in_excluded_section = self.should_ignore_section(&this_section);
in_direct_save_section = self.should_direct_save(&this_section);
if !cur_section.is_empty() && (self.keep_empty_section || !item_group.is_empty()) {
if self.content.contains_key(&cur_section) {
if self.allow_dup_section_titles
|| self.content.get(&cur_section).unwrap().is_empty()
{
if let Some(existing_items) = self.content.get_mut(&cur_section) {
existing_items.extend(item_group.drain(..));
}
} else {
self.last_error = IniReaderError::Duplicate;
return Err(IniReaderError::Duplicate);
}
} else if !in_isolated_section || self.isolated_items_section != this_section {
if !item_group.is_empty() {
read_sections.push(cur_section.clone());
}
if !self.section_order.contains(&cur_section) {
self.section_order.push(cur_section.clone());
}
self.content.insert(cur_section.clone(), item_group);
}
}
in_isolated_section = false;
cur_section = this_section;
item_group = Vec::new();
}
else if !in_excluded_section && !cur_section.is_empty() {
let pos_equal = line.find('=');
if (self.store_any_line && pos_equal.is_none()) || in_direct_save_section {
item_group.push(("{NONAME}".to_string(), line));
}
else if let Some(pos) = pos_equal {
let item_name = line[0..pos].trim().to_string();
let item_value = if pos + 1 < line.len() {
line[pos + 1..].trim().to_string()
} else {
String::new()
};
item_group.push((item_name, item_value));
}
} else if cur_section.is_empty() {
self.last_error = IniReaderError::OutOfBound;
return Err(IniReaderError::OutOfBound);
}
if !self.include_sections.is_empty()
&& read_sections
.iter()
.all(|s| self.include_sections.contains(s))
{
break;
}
}
if !cur_section.is_empty() && (self.keep_empty_section || !item_group.is_empty()) {
if self.content.contains_key(&cur_section) {
if self.allow_dup_section_titles || in_isolated_section {
if let Some(existing_items) = self.content.get_mut(&cur_section) {
existing_items.extend(item_group.drain(..));
}
} else if !self.content.get(&cur_section).unwrap().is_empty() {
self.last_error = IniReaderError::Duplicate;
return Err(IniReaderError::Duplicate);
}
} else if !in_isolated_section || self.isolated_items_section != cur_section {
if !item_group.is_empty() {
read_sections.push(cur_section.clone());
}
if !self.section_order.contains(&cur_section) {
self.section_order.push(cur_section.clone());
}
self.content.insert(cur_section, item_group);
}
}
self.parsed = true;
self.last_error = IniReaderError::None;
Ok(())
}
pub async fn parse_file(&mut self, path: &str) -> Result<(), IniReaderError> {
if !file_exists(path).await {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
let content = file_get_async(path, None).await?;
self.parse(&content)
}
pub fn section_exist(&self, section: &str) -> bool {
self.content.contains_key(section)
}
pub fn section_count(&self) -> usize {
self.content.len()
}
pub fn get_section_names(&self) -> &[String] {
&self.section_order
}
pub fn set_current_section(&mut self, section: &str) {
self.current_section = section.to_string();
}
pub fn enter_section(&mut self, section: &str) -> Result<(), IniReaderError> {
if !self.section_exist(section) {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
self.current_section = section.to_string();
self.last_error = IniReaderError::None;
Ok(())
}
pub fn item_exist(&self, section: &str, item_name: &str) -> bool {
if !self.section_exist(section) {
return false;
}
self.content
.get(section)
.map(|items| items.iter().any(|(key, _)| key == item_name))
.unwrap_or(false)
}
pub fn item_exist_current(&self, item_name: &str) -> bool {
if self.current_section.is_empty() {
return false;
}
self.item_exist(&self.current_section, item_name)
}
pub fn item_prefix_exists(&self, section: &str, prefix: &str) -> bool {
if !self.section_exist(section) {
return false;
}
if let Some(items) = self.content.get(section) {
return items.iter().any(|(key, _)| key.starts_with(prefix));
}
false
}
pub fn item_prefix_exist(&self, prefix: &str) -> bool {
if self.current_section.is_empty() {
return false;
}
self.item_prefix_exists(&self.current_section, prefix)
}
pub fn get_items(&self, section: &str) -> Result<Vec<(String, String)>, IniReaderError> {
if !self.parsed {
return Err(IniReaderError::NotParsed);
}
if !self.section_exist(section) {
return Err(IniReaderError::NotExist);
}
Ok(self.content.get(section).cloned().unwrap_or_default())
}
pub fn get_all(&self, section: &str, item_name: &str) -> Result<Vec<String>, IniReaderError> {
if !self.parsed {
return Err(IniReaderError::NotParsed);
}
if !self.section_exist(section) {
return Err(IniReaderError::NotExist);
}
let mut results = Vec::new();
if let Some(items) = self.content.get(section) {
for (key, value) in items {
if key.starts_with(item_name) {
results.push(value.clone());
}
}
}
Ok(results)
}
pub fn get_all_current(&self, item_name: &str) -> Result<Vec<String>, IniReaderError> {
if self.current_section.is_empty() {
return Err(IniReaderError::NotExist);
}
self.get_all(&self.current_section, item_name)
}
pub fn get(&self, section: &str, item_name: &str) -> String {
if !self.parsed || !self.section_exist(section) {
return String::new();
}
self.content
.get(section)
.and_then(|items| items.iter().find(|(key, _)| key == item_name))
.map(|(_, value)| value.clone())
.unwrap_or_default()
}
pub fn get_current(&self, item_name: &str) -> String {
if self.current_section.is_empty() {
return String::new();
}
self.get(&self.current_section, item_name)
}
pub fn get_bool(&self, section: &str, item_name: &str) -> bool {
self.get(section, item_name) == "true"
}
pub fn get_bool_current(&self, item_name: &str) -> bool {
self.get_current(item_name) == "true"
}
pub fn get_int(&self, section: &str, item_name: &str) -> i32 {
self.get(section, item_name).parse::<i32>().unwrap_or(0)
}
pub fn get_int_current(&self, item_name: &str) -> i32 {
self.get_current(item_name).parse::<i32>().unwrap_or(0)
}
pub fn set(
&mut self,
section: &str,
item_name: &str,
item_val: &str,
) -> Result<(), IniReaderError> {
if section.is_empty() {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
if !self.parsed {
self.parsed = true;
}
let real_section = if section == "{NONAME}" {
if self.current_section.is_empty() {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
&self.current_section
} else {
section
};
if !self.section_exist(real_section) {
self.section_order.push(real_section.to_string());
self.content.insert(real_section.to_string(), Vec::new());
}
if let Some(section_vec) = self.content.get_mut(real_section) {
section_vec.push((item_name.to_string(), item_val.to_string()));
}
self.last_error = IniReaderError::None;
Ok(())
}
pub fn set_current(&mut self, item_name: &str, item_val: &str) -> Result<(), IniReaderError> {
if self.current_section.is_empty() {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
if item_name == "{NONAME}" {
return self.set_current_with_noname(item_val);
}
self.set(&self.current_section.clone(), item_name, item_val)
}
pub fn set_bool(
&mut self,
section: &str,
item_name: &str,
item_val: bool,
) -> Result<(), IniReaderError> {
self.set(section, item_name, if item_val { "true" } else { "false" })
}
pub fn set_bool_current(
&mut self,
item_name: &str,
item_val: bool,
) -> Result<(), IniReaderError> {
if self.current_section.is_empty() {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
let value = if item_val { "true" } else { "false" };
self.set(&self.current_section.clone(), item_name, value)
}
pub fn set_int(
&mut self,
section: &str,
item_name: &str,
item_val: i32,
) -> Result<(), IniReaderError> {
self.set(section, item_name, &item_val.to_string())
}
pub fn set_int_current(
&mut self,
item_name: &str,
item_val: i32,
) -> Result<(), IniReaderError> {
if self.current_section.is_empty() {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
self.set(
&self.current_section.clone(),
item_name,
&item_val.to_string(),
)
}
pub fn to_string(&self) -> String {
if !self.parsed {
return String::new();
}
let mut result = String::new();
for section_name in &self.section_order {
result.push_str(&format!("[{}]\n", section_name));
if let Some(section) = self.content.get(section_name) {
if section.is_empty() {
result.push('\n');
continue;
}
for (key, value) in section {
let mut value = value.clone();
Self::process_escape_char_reverse(&mut value);
if key != "{NONAME}" {
result.push_str(&format!("{}={}\n", key, value));
} else {
result.push_str(&format!("{}\n", value));
}
}
result.push('\n');
}
}
result
}
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
if !self.parsed {
return Err(io::Error::new(io::ErrorKind::InvalidData, "INI not parsed"));
}
let content = self.to_string();
std::fs::write(path, content)?;
Ok(())
}
pub fn set_current_with_noname(&mut self, item_val: &str) -> Result<(), IniReaderError> {
if self.current_section.is_empty() {
self.last_error = IniReaderError::NotExist;
return Err(IniReaderError::NotExist);
}
self.set(&self.current_section.clone(), "{NONAME}", item_val)
}
}