zoi/pkg/lua/
functions.rs

1use crate::utils;
2use flate2::read::GzDecoder;
3use md5;
4use mlua::{self, Lua, LuaSerdeExt, Table, Value};
5use sequoia_openpgp::{Cert, parse::Parse};
6use serde::Deserialize;
7use sha2::{Digest, Sha256, Sha512};
8use std::io::Read;
9use std::path::PathBuf;
10use std::{fs, path::Path};
11use urlencoding;
12use walkdir::WalkDir;
13use xz2::read::XzDecoder;
14use zip::ZipArchive;
15use zstd::stream::read::Decoder as ZstdDecoder;
16
17fn add_parse_util(lua: &Lua) -> Result<(), mlua::Error> {
18    let parse_table = lua.create_table()?;
19
20    let json_fn = lua.create_function(|lua, json_str: String| {
21        let value: serde_json::Value = serde_json::from_str(&json_str)
22            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
23        lua.to_value(&value)
24    })?;
25    parse_table.set("json", json_fn)?;
26
27    let yaml_fn = lua.create_function(|lua, yaml_str: String| {
28        let value: serde_yaml::Value = serde_yaml::from_str(&yaml_str)
29            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
30        lua.to_value(&value)
31    })?;
32    parse_table.set("yaml", yaml_fn)?;
33
34    let toml_fn = lua.create_function(|lua, toml_str: String| {
35        let value: toml::Value =
36            toml::from_str(&toml_str).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
37        lua.to_value(&value)
38    })?;
39    parse_table.set("toml", toml_fn)?;
40
41    let checksum_fn = lua.create_function(|_, (content, file_name): (String, String)| {
42        for line in content.lines() {
43            let parts: Vec<&str> = line.split_whitespace().collect();
44            if parts.len() == 2 && parts[1] == file_name {
45                return Ok(Some(parts[0].to_string()));
46            }
47        }
48        Ok(None)
49    })?;
50    parse_table.set("checksumFile", checksum_fn)?;
51
52    let utils_table: Table = lua.globals().get("UTILS")?;
53    utils_table.set("PARSE", parse_table)?;
54
55    Ok(())
56}
57
58fn add_fetch_util(lua: &Lua) -> Result<(), mlua::Error> {
59    let fetch_table = lua.create_table()?;
60
61    let fetch_fn = lua.create_function(|_, url: String| -> Result<String, mlua::Error> {
62        let response =
63            reqwest::blocking::get(url).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
64        let text = response
65            .text()
66            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
67        Ok(text)
68    })?;
69    fetch_table.set("url", fetch_fn)?;
70
71    let utils_table: Table = lua.globals().get("UTILS")?;
72    utils_table.set("FETCH", fetch_table)?;
73
74    Ok(())
75}
76
77#[derive(Deserialize)]
78struct GitArgs {
79    repo: String,
80    domain: Option<String>,
81    branch: Option<String>,
82}
83
84fn fetch_json(url: &str) -> Result<serde_json::Value, mlua::Error> {
85    let client = reqwest::blocking::Client::builder()
86        .user_agent("zoi")
87        .build()
88        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
89
90    let response = client
91        .get(url)
92        .send()
93        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
94
95    if !response.status().is_success() {
96        return Err(mlua::Error::RuntimeError(format!(
97            "Request to {} failed with status: {} and body: {}",
98            url,
99            response.status(),
100            response.text().unwrap_or_else(|_| "N/A".to_string())
101        )));
102    }
103
104    let text = response
105        .text()
106        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
107    serde_json::from_str(&text).map_err(|e| mlua::Error::RuntimeError(e.to_string()))
108}
109
110fn add_git_fetch_util(lua: &Lua) -> Result<(), mlua::Error> {
111    let utils_table: Table = lua.globals().get("UTILS")?;
112    let fetch_table: Table = utils_table.get("FETCH")?;
113
114    for provider in ["GITHUB", "GITLAB", "GITEA", "FORGEJO"] {
115        let provider_table = lua.create_table()?;
116        let latest_table = lua.create_table()?;
117
118        for what in ["tag", "release", "commit"] {
119            let get_latest_fn = lua.create_function(move |lua, args: Table| {
120                let git_args: GitArgs = lua
121                    .from_value(Value::Table(args))
122                    .map_err(|e| mlua::Error::RuntimeError(format!("Invalid arguments: {}", e)))?;
123
124                let base_url = match provider {
125                    "GITHUB" => git_args
126                        .domain
127                        .unwrap_or_else(|| "https://api.github.com".to_string()),
128                    "GITLAB" => git_args
129                        .domain
130                        .unwrap_or_else(|| "https://gitlab.com".to_string()),
131                    "GITEA" => git_args
132                        .domain
133                        .unwrap_or_else(|| "https://gitea.com".to_string()),
134                    "FORGEJO" => git_args
135                        .domain
136                        .unwrap_or_else(|| "https://codeberg.org".to_string()),
137                    _ => unreachable!(),
138                };
139
140                let url = match (provider, what) {
141                    ("GITHUB", "tag") => format!("{}/repos/{}/tags", base_url, git_args.repo),
142                    ("GITHUB", "release") => {
143                        format!("{}/repos/{}/releases/latest", base_url, git_args.repo)
144                    }
145                    ("GITHUB", "commit") => format!(
146                        "{}/repos/{}/commits?sha={}",
147                        base_url,
148                        git_args.repo,
149                        git_args.branch.as_deref().unwrap_or("HEAD")
150                    ),
151
152                    ("GITLAB", "tag") => format!(
153                        "{}/api/v4/projects/{}/repository/tags",
154                        base_url,
155                        urlencoding::encode(&git_args.repo)
156                    ),
157                    ("GITLAB", "release") => format!(
158                        "{}/api/v4/projects/{}/releases",
159                        base_url,
160                        urlencoding::encode(&git_args.repo)
161                    ),
162                    ("GITLAB", "commit") => format!(
163                        "{}/api/v4/projects/{}/repository/commits?ref_name={}",
164                        base_url,
165                        urlencoding::encode(&git_args.repo),
166                        git_args.branch.as_deref().unwrap_or("HEAD")
167                    ),
168
169                    ("GITEA" | "FORGEJO", "tag") => {
170                        format!("{}/api/v1/repos/{}/tags", base_url, git_args.repo)
171                    }
172                    ("GITEA" | "FORGEJO", "release") => {
173                        format!(
174                            "{}/api/v1/repos/{}/releases/latest",
175                            base_url, git_args.repo
176                        )
177                    }
178                    ("GITEA" | "FORGEJO", "commit") => format!(
179                        "{}/api/v1/repos/{}/commits?sha={}",
180                        base_url,
181                        git_args.repo,
182                        git_args.branch.as_deref().unwrap_or("HEAD")
183                    ),
184                    _ => unreachable!(),
185                };
186
187                let json = fetch_json(&url)?;
188
189                let result = match (provider, what) {
190                    ("GITHUB", "tag") | ("GITEA", "tag") | ("FORGEJO", "tag") => json
191                        .as_array()
192                        .and_then(|a| a.first())
193                        .and_then(|t| t["name"].as_str()),
194                    ("GITHUB", "release") | ("GITEA", "release") | ("FORGEJO", "release") => {
195                        json["tag_name"].as_str()
196                    }
197                    ("GITHUB", "commit") | ("GITEA", "commit") | ("FORGEJO", "commit") => json
198                        .as_array()
199                        .and_then(|a| a.first())
200                        .and_then(|c| c["sha"].as_str()),
201
202                    ("GITLAB", "tag") => json
203                        .as_array()
204                        .and_then(|a| a.first())
205                        .and_then(|t| t["name"].as_str()),
206                    ("GITLAB", "release") => json
207                        .as_array()
208                        .and_then(|a| a.first())
209                        .and_then(|r| r["tag_name"].as_str()),
210                    ("GITLAB", "commit") => json
211                        .as_array()
212                        .and_then(|a| a.first())
213                        .and_then(|c| c["id"].as_str()),
214                    _ => unreachable!(),
215                };
216
217                result.map(|s| s.to_string()).ok_or_else(|| {
218                    mlua::Error::RuntimeError(
219                        "Could not extract value from API response".to_string(),
220                    )
221                })
222            })?;
223            latest_table.set(what, get_latest_fn)?;
224        }
225
226        provider_table.set("LATEST", latest_table)?;
227        fetch_table.set(provider, provider_table)?;
228    }
229
230    Ok(())
231}
232
233fn add_file_util(lua: &Lua) -> Result<(), mlua::Error> {
234    let file_fn = lua.create_function(
235        |_, (url, path): (String, String)| -> Result<(), mlua::Error> {
236            let response = reqwest::blocking::get(url)
237                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
238            let content = response
239                .bytes()
240                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
241            fs::write(path, content).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
242            Ok(())
243        },
244    )?;
245
246    let utils_table: Table = lua.globals().get("UTILS")?;
247    utils_table.set("FILE", file_fn)?;
248
249    Ok(())
250}
251
252fn add_import_util(lua: &Lua, current_path: &Path) -> Result<(), mlua::Error> {
253    let current_path_buf = current_path.to_path_buf();
254    let import_fn = lua.create_function(move |lua, file_name: String| {
255        let path = current_path_buf.parent().unwrap().join(&file_name);
256        let content =
257            fs::read_to_string(&path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
258
259        if let Some(extension) = path.extension().and_then(|s| s.to_str()) {
260            match extension {
261                "json" => {
262                    let value: serde_json::Value = serde_json::from_str(&content)
263                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
264                    return lua.to_value(&value);
265                }
266                "yaml" | "yml" => {
267                    let value: serde_yaml::Value = serde_yaml::from_str(&content)
268                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
269                    return lua.to_value(&value);
270                }
271                "toml" => {
272                    let value: toml::Value = toml::from_str(&content)
273                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
274                    return lua.to_value(&value);
275                }
276                _ => {
277                    return lua.to_value(&content);
278                }
279            }
280        }
281
282        lua.to_value(&content)
283    })?;
284    lua.globals().set("IMPORT", import_fn)?;
285    Ok(())
286}
287
288fn add_include_util(lua: &Lua, current_path: &Path) -> Result<(), mlua::Error> {
289    let current_path_buf = current_path.to_path_buf();
290    let include_fn =
291        lua.create_function(move |lua, file_name: String| -> Result<(), mlua::Error> {
292            let path = current_path_buf.parent().unwrap().join(file_name);
293            let code =
294                fs::read_to_string(path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
295            lua.load(&code).exec()?;
296            Ok(())
297        })?;
298    lua.globals().set("INCLUDE", include_fn)?;
299    Ok(())
300}
301
302fn add_zcp(lua: &Lua) -> Result<(), mlua::Error> {
303    let zcp_fn = lua.create_function(|lua, (source, destination): (String, String)| {
304        let ops_table: Table = match lua.globals().get("__ZoiBuildOperations") {
305            Ok(t) => t,
306            Err(_) => {
307                let new_t = lua.create_table()?;
308                lua.globals().set("__ZoiBuildOperations", new_t.clone())?;
309                new_t
310            }
311        };
312        let op = lua.create_table()?;
313        op.set("op", "zcp")?;
314        op.set("source", source)?;
315        op.set("destination", destination)?;
316        ops_table.push(op)?;
317        Ok(())
318    })?;
319    lua.globals().set("zcp", zcp_fn)?;
320    Ok(())
321}
322
323fn add_verify_hash(lua: &Lua) -> Result<(), mlua::Error> {
324    let verify_hash_fn = lua.create_function(|_lua, (file_path, hash_str): (String, String)| {
325        let parts: Vec<&str> = hash_str.splitn(2, '-').collect();
326        if parts.len() != 2 {
327            return Err(mlua::Error::RuntimeError(
328                "Invalid hash format. Expected 'algo-hash'".to_string(),
329            ));
330        }
331        let algo = parts[0];
332        let expected_hash = parts[1];
333
334        let mut file = fs::File::open(&file_path)
335            .map_err(|e| mlua::Error::RuntimeError(format!("Failed to open file: {}", e)))?;
336        let mut buffer = Vec::new();
337        file.read_to_end(&mut buffer)
338            .map_err(|e| mlua::Error::RuntimeError(format!("Failed to read file: {}", e)))?;
339
340        let actual_hash = match algo {
341            "md5" => {
342                let digest = md5::compute(&buffer);
343                format!("{:x}", digest)
344            }
345            "sha256" => {
346                let mut hasher = Sha256::new();
347                hasher.update(&buffer);
348                hex::encode(hasher.finalize())
349            }
350            "sha512" => {
351                let mut hasher = Sha512::new();
352                hasher.update(&buffer);
353                hex::encode(hasher.finalize())
354            }
355            _ => {
356                return Err(mlua::Error::RuntimeError(format!(
357                    "Unsupported hash algorithm: {}",
358                    algo
359                )));
360            }
361        };
362
363        if actual_hash.eq_ignore_ascii_case(expected_hash) {
364            Ok(true)
365        } else {
366            println!(
367                "Hash mismatch for {}: expected {}, got {}",
368                file_path, expected_hash, actual_hash
369            );
370            Ok(false)
371        }
372    })?;
373    lua.globals().set("verifyHash", verify_hash_fn)?;
374    Ok(())
375}
376
377fn add_zrm(lua: &Lua) -> Result<(), mlua::Error> {
378    let zrm_fn = lua.create_function(|lua, path: String| {
379        let ops_table: Table = match lua.globals().get("__ZoiUninstallOperations") {
380            Ok(t) => t,
381            Err(_) => {
382                let new_t = lua.create_table()?;
383                lua.globals()
384                    .set("__ZoiUninstallOperations", new_t.clone())?;
385                new_t
386            }
387        };
388        let op = lua.create_table()?;
389        op.set("op", "zrm")?;
390        op.set("path", path)?;
391        ops_table.push(op)?;
392        Ok(())
393    })?;
394    lua.globals().set("zrm", zrm_fn)?;
395    Ok(())
396}
397
398fn add_cmd_util(lua: &Lua) -> Result<(), mlua::Error> {
399    let cmd_fn = lua.create_function(|_, command: String| {
400        println!("Executing: {}", command);
401        let output = if cfg!(target_os = "windows") {
402            std::process::Command::new("pwsh")
403                .arg("-Command")
404                .arg(&command)
405                .output()
406        } else {
407            std::process::Command::new("bash")
408                .arg("-c")
409                .arg(&command)
410                .output()
411        };
412
413        match output {
414            Ok(out) => {
415                if !out.status.success() {
416                    eprintln!("[cmd] {}", String::from_utf8_lossy(&out.stderr));
417                }
418                Ok(out.status.success())
419            }
420            Err(e) => {
421                eprintln!("[cmd] Failed to execute command: {}", e);
422                Ok(false)
423            }
424        }
425    })?;
426    lua.globals().set("cmd", cmd_fn)?;
427    Ok(())
428}
429
430fn add_find_util(lua: &Lua) -> Result<(), mlua::Error> {
431    let find_table = lua.create_table()?;
432
433    let find_file_fn = lua.create_function(|lua, (dir, name): (String, String)| {
434        let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
435        let search_dir = Path::new(&build_dir_str).join(dir);
436        for entry in WalkDir::new(search_dir) {
437            let entry = entry.map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
438            if entry.file_name().to_string_lossy() == name {
439                let path = entry.path();
440                let relative_path = path.strip_prefix(Path::new(&build_dir_str)).unwrap();
441                return Ok(Some(relative_path.to_string_lossy().to_string()));
442            }
443        }
444        Ok(None)
445    })?;
446    find_table.set("file", find_file_fn)?;
447
448    let utils_table: Table = lua.globals().get("UTILS")?;
449    utils_table.set("FIND", find_table)?;
450
451    Ok(())
452}
453
454fn add_extract_util(lua: &Lua) -> Result<(), mlua::Error> {
455    let extract_fn = lua.create_function(|lua, (source, out_name): (String, Option<String>)| {
456        let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
457        let build_dir = Path::new(&build_dir_str);
458
459        let archive_file = if source.starts_with("http") {
460            println!("Downloading: {}", source);
461            let file_name = source.split('/').next_back().unwrap_or("download.tmp");
462            let temp_path = build_dir.join(file_name);
463            let response = reqwest::blocking::get(&source)
464                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
465            let content = response
466                .bytes()
467                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
468            fs::write(&temp_path, content).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
469            temp_path
470        } else {
471            PathBuf::from(source)
472        };
473
474        let out_dir_name = out_name.unwrap_or_else(|| "extracted".to_string());
475        let out_dir = build_dir.join(out_dir_name);
476        fs::create_dir_all(&out_dir).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
477
478        println!(
479            "Extracting {} to {}",
480            archive_file.display(),
481            out_dir.display()
482        );
483
484        let file =
485            fs::File::open(&archive_file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
486
487        let archive_path_str = archive_file.to_string_lossy();
488
489        if archive_path_str.ends_with(".zip") {
490            let mut archive =
491                ZipArchive::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
492            archive
493                .extract(&out_dir)
494                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
495        } else if archive_path_str.ends_with(".tar.gz") {
496            let tar_gz = GzDecoder::new(file);
497            let mut archive = tar::Archive::new(tar_gz);
498            archive
499                .unpack(&out_dir)
500                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
501        } else if archive_path_str.ends_with(".tar.zst") {
502            let tar_zst =
503                ZstdDecoder::new(file).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
504            let mut archive = tar::Archive::new(tar_zst);
505            archive
506                .unpack(&out_dir)
507                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
508        } else if archive_path_str.ends_with(".tar.xz") {
509            let tar_xz = XzDecoder::new(file);
510            let mut archive = tar::Archive::new(tar_xz);
511            archive
512                .unpack(&out_dir)
513                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
514        } else {
515            return Err(mlua::Error::RuntimeError(format!(
516                "Unsupported archive format for file: {}",
517                archive_path_str
518            )));
519        }
520
521        Ok(())
522    })?;
523
524    let utils_table: Table = lua.globals().get("UTILS")?;
525    utils_table.set("EXTRACT", extract_fn)?;
526
527    Ok(())
528}
529
530fn add_verify_signature(lua: &Lua) -> Result<(), mlua::Error> {
531    let verify_sig_fn = lua.create_function(
532        |_, (file_path, sig_path, key_source): (String, String, String)| {
533            let key_bytes: Vec<u8> = if key_source.starts_with("http") {
534                match reqwest::blocking::get(&key_source).and_then(|r| r.bytes()) {
535                    Ok(b) => b.to_vec(),
536                    Err(e) => {
537                        return Err(mlua::Error::RuntimeError(format!(
538                            "Failed to download key: {}",
539                            e
540                        )));
541                    }
542                }
543            } else if Path::new(&key_source).exists() {
544                match fs::read(&key_source) {
545                    Ok(b) => b,
546                    Err(e) => {
547                        return Err(mlua::Error::RuntimeError(format!(
548                            "Failed to read key file: {}",
549                            e
550                        )));
551                    }
552                }
553            } else {
554                let pgp_dir = match crate::pkg::pgp::get_pgp_dir() {
555                    Ok(dir) => dir,
556                    Err(e) => {
557                        return Err(mlua::Error::RuntimeError(format!(
558                            "Failed to get PGP dir: {}",
559                            e
560                        )));
561                    }
562                };
563                let key_path = pgp_dir.join(format!("{}.asc", key_source));
564                if !key_path.exists() {
565                    return Err(mlua::Error::RuntimeError(format!(
566                        "Key with name '{}' not found.",
567                        key_source
568                    )));
569                }
570                match fs::read(&key_path) {
571                    Ok(b) => b,
572                    Err(e) => {
573                        return Err(mlua::Error::RuntimeError(format!(
574                            "Failed to read key file: {}",
575                            e
576                        )));
577                    }
578                }
579            };
580
581            let cert = match Cert::from_bytes(&key_bytes) {
582                Ok(c) => c,
583                Err(e) => return Err(mlua::Error::RuntimeError(format!("Invalid PGP key: {}", e))),
584            };
585
586            let result = crate::pkg::pgp::verify_detached_signature(
587                Path::new(&file_path),
588                Path::new(&sig_path),
589                &cert,
590            );
591
592            match result {
593                Ok(_) => Ok(true),
594                Err(e) => {
595                    eprintln!("Signature verification failed: {}", e);
596                    Ok(false)
597                }
598            }
599        },
600    )?;
601    lua.globals().set("verifySignature", verify_sig_fn)?;
602    Ok(())
603}
604
605fn add_add_pgp_key(lua: &Lua) -> Result<(), mlua::Error> {
606    let add_pgp_key_fn = lua.create_function(|_, (source, name): (String, String)| {
607        let result = if source.starts_with("http") {
608            crate::pkg::pgp::add_key_from_url(&source, &name)
609        } else {
610            crate::pkg::pgp::add_key_from_path(&source, Some(&name))
611        };
612
613        if let Err(e) = result {
614            eprintln!("Failed to add PGP key '{}': {}", name, e);
615            return Ok(false);
616        }
617        Ok(true)
618    })?;
619    lua.globals().set("addPgpKey", add_pgp_key_fn)?;
620    Ok(())
621}
622
623pub fn add_package_lifecycle_functions(lua: &Lua) -> Result<(), mlua::Error> {
624    let metadata_fn = lua.create_function(move |lua, pkg_def: Table| {
625        if let Ok(meta_table) = lua.globals().get::<Table>("__ZoiPackageMeta")
626            && let Ok(pkg_global) = lua.globals().get::<Table>("PKG")
627        {
628            for pair in pkg_def.pairs::<Value, Value>() {
629                let (key, value) = pair?;
630                meta_table.set(key.clone(), value.clone())?;
631                pkg_global.set(key, value)?;
632            }
633        }
634        Ok(())
635    })?;
636    lua.globals().set("metadata", metadata_fn)?;
637
638    let dependencies_fn = lua.create_function(move |lua, deps_def: Table| {
639        if let Ok(deps_table) = lua.globals().get::<Table>("__ZoiPackageDeps") {
640            for pair in deps_def.pairs::<String, Value>() {
641                let (key, value) = pair?;
642                deps_table.set(key, value)?;
643            }
644        }
645        Ok(())
646    })?;
647    lua.globals().set("dependencies", dependencies_fn)?;
648
649    let updates_fn = lua.create_function(move |lua, updates_list: Table| {
650        if let Ok(updates_table) = lua.globals().get::<Table>("__ZoiPackageUpdates") {
651            for pair in updates_list.pairs::<Value, Table>() {
652                let (_, update_info) = pair?;
653                updates_table.push(update_info)?;
654            }
655        }
656        Ok(())
657    })?;
658    lua.globals().set("updates", updates_fn)?;
659
660    let hooks_fn = lua.create_function(move |lua, hooks_def: Table| {
661        if let Ok(hooks_table) = lua.globals().get::<Table>("__ZoiPackageHooks") {
662            for pair in hooks_def.pairs::<String, Value>() {
663                let (key, value) = pair?;
664                hooks_table.set(key, value)?;
665            }
666        }
667        Ok(())
668    })?;
669    lua.globals().set("hooks", hooks_fn)?;
670
671    let prepare_fn = lua.create_function(|_, _: Table| Ok(()))?;
672    lua.globals().set("prepare", prepare_fn)?;
673    let package_fn = lua.create_function(|_, _: Table| Ok(()))?;
674    lua.globals().set("package", package_fn)?;
675    let verify_fn = lua.create_function(|_, _: Table| Ok(()))?;
676    lua.globals().set("verify", verify_fn)?;
677    let uninstall_fn = lua.create_function(|_, _: Table| Ok(()))?;
678    lua.globals().set("uninstall", uninstall_fn)?;
679
680    Ok(())
681}
682
683pub fn setup_lua_environment(
684    lua: &Lua,
685    platform: &str,
686    version_override: Option<&str>,
687    file_path: Option<&str>,
688    create_pkg_dir: Option<&str>,
689) -> Result<(), mlua::Error> {
690    let system_table = lua.create_table()?;
691    let parts: Vec<&str> = platform.split('-').collect();
692    system_table.set("OS", *parts.first().unwrap_or(&""))?;
693    system_table.set("ARCH", *parts.get(1).unwrap_or(&""))?;
694    if let Some(distro) = utils::get_linux_distribution() {
695        system_table.set("DISTRO", distro)?;
696    }
697    if let Some(manager) = utils::get_native_package_manager() {
698        system_table.set("MANAGER", manager)?;
699    }
700    lua.globals().set("SYSTEM", system_table)?;
701
702    let zoi_table = lua.create_table()?;
703    if let Some(ver) = version_override {
704        zoi_table.set("VERSION", ver)?;
705    }
706
707    if let Some(dir) = create_pkg_dir {
708        zoi_table.set("CREATE_PKG_DIR", dir)?;
709    }
710
711    let path_table = lua.create_table()?;
712    if let Some(home_dir) = home::home_dir() {
713        path_table.set("user", home_dir.join(".zoi").to_string_lossy().to_string())?;
714    }
715
716    let system_bin_path = if cfg!(target_os = "windows") {
717        "C:\\ProgramData\\zoi\\pkgs\\bin".to_string()
718    } else {
719        "/usr/local/bin".to_string()
720    };
721    path_table.set("system", system_bin_path)?;
722
723    zoi_table.set("PATH", path_table)?;
724    lua.globals().set("ZOI", zoi_table)?;
725
726    let utils_table = lua.create_table()?;
727    lua.globals().set("UTILS", utils_table)?;
728
729    add_fetch_util(lua)?;
730    add_parse_util(lua)?;
731    add_git_fetch_util(lua)?;
732    add_file_util(lua)?;
733    add_zcp(lua)?;
734    add_verify_hash(lua)?;
735    add_zrm(lua)?;
736    add_cmd_util(lua)?;
737    add_find_util(lua)?;
738    add_extract_util(lua)?;
739    add_verify_signature(lua)?;
740    add_add_pgp_key(lua)?;
741    add_package_lifecycle_functions(lua)?;
742
743    if let Some(path_str) = file_path {
744        let path = Path::new(path_str);
745        add_import_util(lua, path)?;
746        add_include_util(lua, path)?;
747    }
748
749    Ok(())
750}