jsonpiler/
functions.rs

1//! Utility functions.
2use crate::{ErrOR, ErrorInfo, JValue, Json, Jsonpiler};
3use core::fmt::{self, Display, Write as _};
4use std::{
5  env, fs,
6  path::Path,
7  process::{Command, exit},
8};
9/// Decoding base64 variants.
10/// # Errors
11/// `Box<dyn Error(String)>` - If an invalid encoded value is passed, return `Err`
12#[expect(dead_code, reason = "todo")]
13pub(crate) fn de64(encoded: &str) -> ErrOR<Vec<u8>> {
14  const ERR: &str = "Unreachable (de64)";
15  let mut decoded = Vec::new();
16  let mut buffer = 0u32;
17  let mut buffer_len = 0u32;
18  for ch in encoded.chars() {
19    if !('0'..='o').contains(&ch) {
20      return Err("Invalid character in input string.".into());
21    }
22    let val = u32::from(ch).checked_sub(48).ok_or(ERR)?;
23    buffer = (buffer << 6u32) | val;
24    buffer_len = buffer_len.checked_add(6).ok_or(ERR)?;
25    while buffer_len >= 8 {
26      let shift = buffer_len.checked_sub(8).ok_or(ERR)?;
27      let byte = u8::try_from(buffer >> shift)?;
28      decoded.push(byte);
29      buffer_len = shift;
30      buffer &= (1u32 << buffer_len).checked_sub(1).ok_or(ERR)?;
31    }
32  }
33  Ok(decoded)
34}
35/// Encoding base64 variants.
36/// # Errors
37/// Unreachable.
38#[expect(dead_code, reason = "todo")]
39pub(crate) fn en64(input: &[u8]) -> Result<String, &str> {
40  const ERR: &str = "Unreachable (en64)";
41  let mut encoded = String::new();
42  let mut helper = |enc: u8| {
43    encoded.push(char::from_u32(u32::from(enc).checked_add(48).ok_or(ERR)?).ok_or(ERR)?);
44    Ok(())
45  };
46  let chunks = input.chunks(3);
47  for chunk in chunks {
48    let b0 = chunk.first().unwrap_or(&0u8);
49    let b1 = chunk.get(1).unwrap_or(&0u8);
50    helper((b0 >> 2u8) & 0x3F)?;
51    helper(((b0 << 4u8) | (b1 >> 4u8)) & 0x3F)?;
52    if chunk.len() >= 2 {
53      let b2 = chunk.get(2).unwrap_or(&0u8);
54      helper(((b1 << 2u8) | (b2 >> 6u8)) & 0x3F)?;
55      if chunk.len() == 3 {
56        helper(b2 & 0x3F)?;
57      }
58    }
59  }
60  Ok(encoded)
61}
62/// Exit the program with exit code 1.
63#[expect(clippy::print_stderr, reason = "")]
64pub(crate) fn error_exit(text: &str) -> ! {
65  eprintln!("{text}");
66  exit(-1)
67}
68/// Escapes special characters in a string for proper JSON formatting.
69/// This method ensures that characters like quotes (`"`) and backslashes (`\`)
70/// are escaped in a way that conforms to the JSON specification.
71/// It also escapes control characters and non-ASCII characters using Unicode escapes.
72/// # Arguments
73/// * `s` - The string to be escaped.
74/// # Errors
75/// * `fmt::Error` - ...
76/// # Returns
77/// * `String` - The escaped string.
78pub(crate) fn escape_string(unescaped: &str) -> Result<String, fmt::Error> {
79  let mut escaped = String::new();
80  escaped.push('"');
81  for ch in unescaped.chars() {
82    match ch {
83      '"' => write!(escaped, r#"\""#)?,
84      '\\' => write!(escaped, r"\\")?,
85      '\n' => write!(escaped, r"\n")?,
86      '\t' => write!(escaped, r"\t")?,
87      '\r' => write!(escaped, r"\r")?,
88      '\u{08}' => write!(escaped, r"\b")?,
89      '\u{0C}' => write!(escaped, r"\f")?,
90      u_ch if u_ch < '\u{20}' => write!(escaped, r"\u{:04x}", u32::from(ch))?,
91      _ => escaped.push(ch),
92    }
93  }
94  escaped.push('"');
95  Ok(escaped)
96}
97/// Change the value of another Json to create a new Json.
98#[must_use]
99pub(crate) const fn obj_json(val: JValue, e_info: ErrorInfo) -> Json {
100  Json { info: e_info, value: val }
101}
102/// Compiles and runs a JSON-based program using the Jsonpiler.
103/// This function performs the following steps:
104/// 1. Parses the first CLI argument as the input JSON file path.
105/// 2. Reads the file content into a string.
106/// 3. Parses it into a `Json` structure.
107/// 4. Compiles it into assembly.
108/// 5. Assembles to `.obj`.
109/// 6. Links to `.exe`.
110/// 7. Executes the `.exe`.
111/// 8. Exits with its exit code.
112/// # Panics
113/// Panics if:
114/// - Not on Windows
115/// - Incorrect CLI arguments
116/// - File read, parse, compile, assemble, link, execute, or wait fails
117/// - Invalid filename or working directory
118/// # Notes
119/// Requires `as` and `ld` in PATH.
120/// Terminates with `error_exit` on failure (exit code 1).
121/// # Example
122/// ```sh
123/// ./jsonpiler test.json
124/// ```
125/// # Platform
126/// Windows only.
127#[inline]
128pub fn run() -> ! {
129  #[cfg(all(not(doc), not(target_os = "windows")))]
130  compile_error!("This program can only run on Windows.");
131  let args: Vec<String> = env::args().collect();
132  let Some(program_name) = args.first() else { error_exit("Failed to get name of the program") };
133  let input_file = unwrap_or_exit(
134    args.get(1).ok_or_else(|| format!("{program_name} <input json file> [arguments of .exe...]")),
135    "Usage",
136  );
137  let source =
138    unwrap_or_exit(fs::read_to_string(input_file), &format!("Failed to read file '{input_file}'"));
139  let mut jsonpiler = Jsonpiler::default();
140  let file = Path::new(input_file);
141  let asm = &file.with_extension("s").to_string_lossy().to_string();
142  let obj = &file.with_extension("obj").to_string_lossy().to_string();
143  let exe = &file.with_extension("exe").to_string_lossy().to_string();
144  unwrap_or_exit(jsonpiler.build(source, input_file, asm), "Error");
145  (!Command::new("as")
146    .args([asm, "-o", obj])
147    .status()
148    .unwrap_or_else(|err| error_exit(&format!("Failed to assemble: {err}")))
149    .success())
150  .then(|| error_exit("Assembling process returned Bad status."));
151  #[cfg(not(debug_assertions))]
152  {
153    unwrap_or_exit(fs::remove_file(asm), &format!("Failed to remove '{asm}'"))
154  }
155  (!Command::new("ld")
156    .args([
157      obj,
158      "-o",
159      exe,
160      "-LC:/Windows/System32",
161      "-luser32",
162      "-lkernel32",
163      "-lucrtbase",
164      "--gc-sections",
165      "-e_start",
166    ])
167    .status()
168    .unwrap_or_else(|err| error_exit(&format!("Failed to link: {err}")))
169    .success())
170  .then(|| error_exit("Linking process returned Bad status."));
171  unwrap_or_exit(fs::remove_file(obj), &format!("Failed to remove '{obj}'"));
172  let exit_code =
173    Command::new(unwrap_or_exit(env::current_dir(), "Failed to get current directory").join(exe))
174      .args(args.get(2..).unwrap_or(&[]))
175      .spawn()
176      .unwrap_or_else(|err| error_exit(&format!("Failed to spawn child process: {err}")))
177      .wait()
178      .unwrap_or_else(|err| error_exit(&format!("Failed to wait for child process: {err}")))
179      .code()
180      .unwrap_or_else(|| error_exit("Failed to retrieve the exit code."));
181  exit(exit_code)
182}
183/// Unwraps the result. Exits the program on error.
184pub(crate) fn unwrap_or_exit<T, U: Display>(result: Result<T, U>, text: &str) -> T {
185  result.unwrap_or_else(|err| error_exit(&format!("{text}: {err}")))
186}