use unilang::data::{ ErrorCode, ErrorData };
use unilang::semantic::VerifiedCommand;
use unilang::types::Value;
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum OutputFormat
{
Text,
Json,
Table,
}
#[ derive( Debug ) ]
pub struct OutputOptions
{
pub format : OutputFormat,
}
impl OutputOptions
{
#[ inline ]
pub fn from_cmd( cmd : &VerifiedCommand ) -> Result< Self, ErrorData >
{
let format = match cmd.arguments.get( "format" )
{
Some( Value::String( s ) ) =>
{
match s.to_ascii_lowercase().as_str()
{
"text" => OutputFormat::Text,
"json" => OutputFormat::Json,
"table" => OutputFormat::Table,
_other =>
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "unknown format '{s}': expected text, json, or table" ),
) );
}
}
}
_ => OutputFormat::Text,
};
Ok( OutputOptions { format } )
}
#[ inline ]
#[ must_use ]
pub fn is_table( &self ) -> bool { matches!( self.format, OutputFormat::Table ) }
}
#[ inline ]
#[ must_use ]
pub fn format_duration_secs( secs : u64 ) -> String
{
if secs == 0
{
return "0m".to_string();
}
let days = secs / 86400;
let hours = ( secs % 86400 ) / 3600;
let mins = ( secs % 3600 ) / 60;
let mut parts = Vec::new();
if days > 0
{
parts.push( format!( "{days}d" ) );
}
if hours > 0
{
parts.push( format!( "{hours}h" ) );
}
if mins > 0 || parts.is_empty()
{
parts.push( format!( "{mins}m" ) );
}
parts.truncate( 2 );
parts.join( " " )
}
#[ inline ]
pub(crate) fn parse_int_flag( cmd : &VerifiedCommand, name : &str, default : i64 ) -> Result< i64, ErrorData >
{
match cmd.arguments.get( name )
{
None => Ok( default ),
Some( Value::Integer( 0 ) ) => Ok( 0 ),
Some( Value::Integer( 1 ) ) => Ok( 1 ),
Some( Value::String( s ) ) if s == "0" => Ok( 0 ),
Some( Value::String( s ) ) if s == "1" => Ok( 1 ),
Some( Value::String( s ) ) if s == "true" => Ok( 1 ),
Some( Value::String( s ) ) if s == "false" => Ok( 0 ),
_ => Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "{name}:: must be 0, 1, false, or true" ),
) ),
}
}
fn base64url_decode( s : &str ) -> Option< Vec< u8 > >
{
let pad = match s.len() % 4 { 0 => 0, 2 => 2, 3 => 1, _ => return None };
let b64 : String = s.chars()
.map( |c| match c { '-' => '+', '_' => '/', c => c } )
.chain( core::iter::repeat( '=' ).take( pad ) )
.collect();
const ALPHA : &[ u8 ] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let val = |c : u8| ALPHA.iter().position( |&a| a == c )
.and_then( |v| u32::try_from( v ).ok() );
let bytes = b64.as_bytes();
let mut out = Vec::with_capacity( b64.len() / 4 * 3 );
let mut i = 0;
while i + 3 < bytes.len()
{
let v0 = val( bytes[ i ] )?;
let v1 = val( bytes[ i + 1 ] )?;
out.push( ( ( ( v0 << 2 ) | ( v1 >> 4 ) ) & 0xFF ) as u8 );
if bytes[ i + 2 ] != b'='
{
let v2 = val( bytes[ i + 2 ] )?;
out.push( ( ( ( v1 << 4 ) | ( v2 >> 2 ) ) & 0xFF ) as u8 );
}
if bytes[ i + 3 ] != b'='
{
let v2 = val( bytes[ i + 2 ] )?;
let v3 = val( bytes[ i + 3 ] )?;
out.push( ( ( ( v2 << 6 ) | v3 ) & 0xFF ) as u8 );
}
i += 4;
}
Some( out )
}
#[ must_use ]
#[ inline ]
pub fn jwt_exp_ms( creds_json : &str ) -> Option< u64 >
{
let key = "\"accessToken\":\"";
let start = creds_json.find( key )? + key.len();
let rest = &creds_json[ start.. ];
let end = rest.find( '"' )?;
let token = &rest[ ..end ];
let mut parts = token.splitn( 3, '.' );
let _header = parts.next()?;
let payload_b64 = parts.next()?;
let payload_bytes = base64url_decode( payload_b64 )?;
let payload = core::str::from_utf8( &payload_bytes ).ok()?;
let needle = "\"exp\":";
let after = &payload[ payload.find( needle )? + needle.len().. ];
let digits_end = after.find( |c : char| !c.is_ascii_digit() ).unwrap_or( after.len() );
let exp_secs : u64 = after[ ..digits_end ].parse().ok()?;
Some( exp_secs * 1000 )
}
#[ 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
}