use std::io::{ self, Write };
use std::path::Path;
#[ derive( Debug, PartialEq ) ]
pub enum StoredAs
{
Bool,
Number,
Str,
Raw,
}
#[ inline ]
#[ must_use ]
pub fn infer_type( raw : &str ) -> StoredAs
{
let trimmed = raw.trim_start();
if trimmed.starts_with( '{' ) || trimmed.starts_with( '[' )
{
return StoredAs::Raw;
}
match raw
{
"true" | "false" => StoredAs::Bool,
"null" => StoredAs::Raw,
other =>
{
if other.parse::< i64 >().is_ok()
|| other.parse::< f64 >().is_ok_and( f64::is_finite )
{
StoredAs::Number
}
else
{
StoredAs::Str
}
}
}
}
#[ inline ]
pub fn read_all_settings( path : &Path ) -> Result< Vec< ( String, String ) >, io::Error >
{
let src = std::fs::read_to_string( path )?;
json_parse_flat_object( &src )
}
#[ inline ]
pub fn get_setting( path : &Path, key : &str ) -> Result< Option< String >, io::Error >
{
let pairs = read_all_settings( path )?;
Ok( pairs.into_iter().find( |( k, _ )| k == key ).map( |( _, v )| v ) )
}
#[ inline ]
pub fn set_setting( path : &Path, key : &str, raw_value : &str ) -> Result< StoredAs, io::Error >
{
let mut pairs = read_or_empty( path )?;
upsert_pair( &mut pairs, key, raw_value );
let stored_as = infer_type( raw_value );
let json = json_serialize_flat_object( &pairs );
atomic_write( path, &json )?;
Ok( stored_as )
}
#[ inline ]
pub fn set_env_var( path : &Path, key : &str, value : &str ) -> Result< (), io::Error >
{
let mut pairs = read_or_empty( path )?;
let env_idx = pairs.iter().position( |( k, _ )| k == "env" );
let mut env_pairs = match env_idx
{
Some( idx ) => json_parse_flat_object( &pairs[ idx ].1 )?,
None => vec![],
};
upsert_pair( &mut env_pairs, key, value );
let env_json = json_serialize_env_compact( &env_pairs );
match env_idx
{
Some( idx ) => pairs[ idx ].1 = env_json,
None => pairs.push( ( "env".to_string(), env_json ) ),
}
let json = json_serialize_flat_object( &pairs );
atomic_write( path, &json )
}
#[ inline ]
pub fn remove_env_var( path : &Path, key : &str ) -> Result< (), io::Error >
{
let mut pairs = match read_all_settings( path )
{
Ok( p ) => p,
Err( e ) if e.kind() == io::ErrorKind::NotFound => return Ok( () ),
Err( e ) => return Err( e ),
};
let Some( idx ) = pairs.iter().position( |( k, _ )| k == "env" ) else { return Ok( () ) };
let mut env_pairs = json_parse_flat_object( &pairs[ idx ].1 )?;
let before = env_pairs.len();
env_pairs.retain( |( k, _ )| k != key );
if env_pairs.len() == before { return Ok( () ); }
pairs[ idx ].1 = json_serialize_env_compact( &env_pairs );
let json = json_serialize_flat_object( &pairs );
atomic_write( path, &json )
}
fn read_or_empty( path : &Path ) -> Result< Vec< ( String, String ) >, io::Error >
{
match read_all_settings( path )
{
Ok( p ) => Ok( p ),
Err( e ) if e.kind() == io::ErrorKind::NotFound => Ok( vec![] ),
Err( e ) => Err( e ),
}
}
fn upsert_pair( pairs : &mut Vec< ( String, String ) >, key : &str, value : &str )
{
if let Some( entry ) = pairs.iter_mut().find( |( k, _ )| k == key )
{
entry.1 = value.to_string();
}
else
{
pairs.push( ( key.to_string(), value.to_string() ) );
}
}
fn atomic_write( path : &Path, content : &str ) -> Result< (), io::Error >
{
let mut tmp_path = path.to_path_buf();
let filename = tmp_path.file_name()
.ok_or_else( || io::Error::new( io::ErrorKind::InvalidInput, "path has no filename" ) )?
.to_string_lossy()
.into_owned();
tmp_path.set_file_name( format!( "{filename}.tmp" ) );
{
let mut f = std::fs::File::create( &tmp_path )?;
f.write_all( content.as_bytes() )?;
f.flush()?;
}
std::fs::rename( &tmp_path, path )
}
fn json_parse_flat_object( src : &str ) -> Result< Vec< ( String, String ) >, io::Error >
{
let s = src.trim();
if !s.starts_with( '{' ) || !s.ends_with( '}' )
{
return Err( io::Error::new( io::ErrorKind::InvalidData, "expected JSON object" ) );
}
let inner = s[ 1 .. s.len() - 1 ].trim();
if inner.is_empty()
{
return Ok( vec![] );
}
let chars : Vec< char > = inner.chars().collect();
let mut pos = 0;
let mut result = vec![];
while pos < chars.len()
{
skip_ws( &chars, &mut pos );
if pos >= chars.len() { break; }
if chars[ pos ] != '"'
{
return Err( io::Error::new( io::ErrorKind::InvalidData, "expected quoted key" ) );
}
let ( key, next ) = parse_json_string( &chars, pos )?;
pos = next;
skip_ws( &chars, &mut pos );
if pos >= chars.len() || chars[ pos ] != ':'
{
return Err( io::Error::new( io::ErrorKind::InvalidData, "expected ':' after key" ) );
}
pos += 1;
skip_ws( &chars, &mut pos );
let ( value, next ) = parse_json_value( &chars, pos )?;
pos = next;
result.push( ( key, value ) );
skip_ws( &chars, &mut pos );
if pos < chars.len() && chars[ pos ] == ','
{
pos += 1;
}
}
Ok( result )
}
fn skip_ws( chars : &[ char ], pos : &mut usize )
{
while *pos < chars.len() && chars[ *pos ].is_whitespace()
{
*pos += 1;
}
}
fn parse_json_string( chars : &[ char ], start : usize ) -> Result< ( String, usize ), io::Error >
{
let mut result = String::new();
let mut i = start + 1;
while i < chars.len()
{
match chars[ i ]
{
'"' => return Ok( ( result, i + 1 ) ),
'\\' =>
{
i += 1;
if i >= chars.len()
{
return Err( io::Error::new( io::ErrorKind::InvalidData, "unterminated escape" ) );
}
match chars[ i ]
{
'"' => result.push( '"' ),
'\\' => result.push( '\\' ),
'/' => result.push( '/' ),
'n' => result.push( '\n' ),
'r' => result.push( '\r' ),
't' => result.push( '\t' ),
'u' =>
{
if i + 4 < chars.len()
{
let hex : String = chars[ i + 1 ..= i + 4 ].iter().collect();
if let Ok( cp ) = u32::from_str_radix( &hex, 16 )
{
if let Some( c ) = char::from_u32( cp )
{
result.push( c );
}
}
i += 4;
}
}
other =>
{
result.push( '\\' );
result.push( other );
}
}
}
c => result.push( c ),
}
i += 1;
}
Err( io::Error::new( io::ErrorKind::InvalidData, "unterminated string" ) )
}
fn parse_json_value( chars : &[ char ], start : usize ) -> Result< ( String, usize ), io::Error >
{
if start >= chars.len()
{
return Err( io::Error::new( io::ErrorKind::InvalidData, "expected value" ) );
}
match chars[ start ]
{
'"' =>
{
let ( s, end ) = parse_json_string( chars, start )?;
Ok( ( s, end ) )
}
'{' | '[' => consume_balanced( chars, start ),
't' =>
{
check_literal( chars, start, &[ 't', 'r', 'u', 'e' ], "true" )
}
'f' =>
{
check_literal( chars, start, &[ 'f', 'a', 'l', 's', 'e' ], "false" )
}
'n' =>
{
check_literal( chars, start, &[ 'n', 'u', 'l', 'l' ], "null" )
}
c if c == '-' || c.is_ascii_digit() =>
{
let end = start + chars[ start .. ]
.iter()
.take_while( |&&ch| ch == '-' || ch == '.' || ch.is_ascii_digit() || ch == 'e' || ch == 'E' || ch == '+' )
.count();
let num_str : String = chars[ start .. end ].iter().collect();
Ok( ( num_str, end ) )
}
other =>
{
Err( io::Error::new(
io::ErrorKind::InvalidData,
format!( "unexpected value start '{other}'" ),
) )
}
}
}
fn consume_balanced( chars : &[ char ], start : usize ) -> Result< ( String, usize ), io::Error >
{
let open = chars[ start ];
let close = if open == '{' { '}' } else { ']' };
let mut depth = 1_u32;
let mut i = start + 1;
let mut in_string = false;
let mut escape = false;
while i < chars.len() && depth > 0
{
if escape
{
escape = false;
i += 1;
continue;
}
match chars[ i ]
{
'\\' if in_string => { escape = true; }
'"' => { in_string = !in_string; }
c if c == open && !in_string => { depth += 1; }
c if c == close && !in_string => { depth -= 1; }
_ => {}
}
i += 1;
}
if depth != 0
{
return Err( io::Error::new( io::ErrorKind::InvalidData, "unbalanced brackets" ) );
}
let raw : String = chars[ start .. i ].iter().collect();
Ok( ( raw, i ) )
}
fn check_literal(
chars : &[ char ],
start : usize,
literal : &[ char ],
name : &str,
) -> Result< ( String, usize ), io::Error >
{
if chars[ start .. ].starts_with( literal )
{
Ok( ( name.to_string(), start + literal.len() ) )
}
else
{
Err( io::Error::new(
io::ErrorKind::InvalidData,
format!( "expected '{name}'" ),
) )
}
}
fn json_serialize_flat_object( pairs : &[ ( String, String ) ] ) -> String
{
if pairs.is_empty()
{
return "{}".to_string();
}
let entries : Vec< String > = pairs.iter().map( |( k, v ) |
{
let json_key = format!( "\"{}\"", json_escape( k ) );
let json_val = match infer_type( v )
{
StoredAs::Bool | StoredAs::Number | StoredAs::Raw => v.clone(),
StoredAs::Str => format!( "\"{}\"", json_escape( v ) ),
};
format!( "{json_key}: {json_val}" )
} ).collect();
format!( "{{\n {}\n}}", entries.join( ",\n " ) )
}
fn json_serialize_env_compact( pairs : &[ ( String, String ) ] ) -> String
{
if pairs.is_empty()
{
return "{}".to_string();
}
let entries : Vec< String > = pairs.iter().map( |( k, v ) |
format!( "\"{}\": \"{}\"", json_escape( k ), json_escape( v ) )
).collect();
format!( "{{{}}}", entries.join( ", " ) )
}
#[ inline ]
#[ must_use ]
pub fn json_escape( s : &str ) -> String
{
let mut out = String::with_capacity( s.len() );
for ch in s.chars()
{
match ch
{
'"' => out.push_str( "\\\"" ),
'\\' => out.push_str( "\\\\" ),
'\n' => out.push_str( "\\n" ),
'\r' => out.push_str( "\\r" ),
'\t' => out.push_str( "\\t" ),
c => out.push( c ),
}
}
out
}