1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
//  UTILITIES.rs
//    by Lut99
//
//  Created:
//    18 Aug 2022, 14:58:16
//  Last edited:
//    14 Jun 2024, 16:40:49
//  Auto updated?
//    Yes
//
//  Description:
//!   Defines common utilities across the Brane project.
//

use std::borrow::Cow;
use std::fs::{self, DirEntry, ReadDir};
use std::future::Future;
use std::path::{Path, PathBuf};

use humanlog::{DebugMode, HumanLogger};
use log::{debug, warn};
use regex::Regex;
use specifications::container::ContainerInfo;
use specifications::data::{AssetInfo, DataIndex, DataInfo};
use specifications::package::{PackageIndex, PackageInfo};
use tokio::runtime::{Builder, Runtime};
use url::{Host, Url};


/***** TEST HELPERS *****/
/// Defines the path of the tests folder.
pub const TESTS_DIR: &str = "../tests";



/// Collects all .yml files in the 'tests' folder as a single PackageIndex.
///
/// # Returns
/// A [`PackageIndex`] with a collection of all package files in the tests older.
///
/// # Panics
/// This function panics if we failed to do so.
#[inline]
pub fn create_package_index() -> PackageIndex {
    // Simply call `create_package_index_from` with the default tests package directory
    create_package_index_from(PathBuf::from(TESTS_DIR).join("packages"))
}
/// Collects all .yml files in the given folder as a single PackageIndex.
///
/// Note that this function is mostly for testing purposes. Typically, using functions directly on [`PackageIndex`] provides a more canonical experience (mostly relating to error handling).
///
/// # Arguments
/// - `path`: The path to load the packages in.
///
/// # Returns
/// A [`PackageIndex`] with a collection of all package files in the given folder.
///
/// # Panics
/// This function may panic if any of the steps fail.
pub fn create_package_index_from(path: impl AsRef<Path>) -> PackageIndex {
    let path: &Path = path.as_ref();

    // Try to open the folder
    let dir = match fs::read_dir(path) {
        Ok(dir) => dir,
        Err(err) => {
            panic!("Failed to list tests directory '{}': {}", path.display(), err);
        },
    };

    // Start a 'recursive' process where we run all '*.bscript' files.
    let mut infos: Vec<PackageInfo> = vec![];
    let mut todo: Vec<(PathBuf, ReadDir)> = vec![(path.into(), dir)];
    while let Some((path, dir)) = todo.pop() {
        // Iterate through it
        for entry in dir {
            // Attempt to unwrap the entry
            let entry: DirEntry = match entry {
                Ok(entry) => entry,
                Err(err) => {
                    panic!("Failed to read entry in directory '{}': {}", path.display(), err);
                },
            };

            // Check whether it's a directory or not
            if entry.path().is_file() {
                // Check if it ends with '.yml'
                if let Some(ext) = entry.path().extension() {
                    if ext.to_str().unwrap_or("") == "yml" || ext.to_str().unwrap_or("") == "yaml" {
                        let info: ContainerInfo = match ContainerInfo::from_path(entry.path()) {
                            Ok(info) => info,
                            Err(err) => {
                                panic!("Failed to read '{}' as ContainerInfo: {}", entry.path().display(), err);
                            },
                        };
                        infos.push(PackageInfo::from(info));
                    }
                }
            } else if entry.path().is_dir() {
                // Recurse, i.e., list and add to the todo list
                let new_dir = match fs::read_dir(entry.path()) {
                    Ok(dir) => dir,
                    Err(err) => {
                        panic!("Failed to list nested tests directory '{}': {}", entry.path().display(), err);
                    },
                };
                if todo.len() == todo.capacity() {
                    todo.reserve(todo.capacity());
                }
                todo.push((entry.path(), new_dir));
            } else {
                // Dunno what to do with it
                println!("Ignoring entry '{}' in '{}' (unknown entry type)", entry.path().display(), path.display());
            }
        }
    }

    // Done
    match PackageIndex::from_packages(infos) {
        Ok(index) => index,
        Err(err) => {
            panic!("Failed to create package index from package infos: {}", err);
        },
    }
}

/// Collects all data index files in the test folder as a DataIndex.
///
/// # Returns
/// A [`DataIndex`] with a collection of all data files in the tests older.
///
/// # Panics
/// This function panics if we failed to do so.
#[inline]
pub fn create_data_index() -> DataIndex {
    // Simply call `create_data_index_from` with the default tests data directory
    create_data_index_from(PathBuf::from(TESTS_DIR).join("data"))
}
/// Collects all data index files in the given folder as a single DataIndex.
///
/// Note that this function is mostly for testing purposes. Typically, using functions directly on [`DataIndex`] provides a more canonical experience (mostly relating to error handling).
///
/// # Arguments
/// - `path`: The path to load the data in.
///
/// # Returns
/// A [`DataIndex`] with a collection of all data files in the given folder.
///
/// # Panics
/// This function may panic if any of the steps fail.
pub fn create_data_index_from(path: impl AsRef<Path>) -> DataIndex {
    let path: &Path = path.as_ref();

    // Try to open the folder
    let dir = match fs::read_dir(path) {
        Ok(dir) => dir,
        Err(err) => {
            panic!("Failed to list tests directory '{}': {}", path.display(), err);
        },
    };

    // Start a 'recursive' process where we run all '*.bscript' files.
    let mut infos: Vec<DataInfo> = vec![];
    let mut todo: Vec<(PathBuf, ReadDir)> = vec![(path.into(), dir)];
    while let Some((path, dir)) = todo.pop() {
        // Iterate through it
        for entry in dir {
            // Attempt to unwrap the entry
            let entry: DirEntry = match entry {
                Ok(entry) => entry,
                Err(err) => {
                    panic!("Failed to read entry in directory '{}': {}", path.display(), err);
                },
            };

            // Check whether it's a directory or not
            if entry.path().is_file() {
                // Check if it ends with '.yml'
                if let Some(ext) = entry.path().extension() {
                    if ext.to_str().unwrap_or("") == "yml" || ext.to_str().unwrap_or("") == "yaml" {
                        // Read it as a DataInfo
                        let info: AssetInfo = match AssetInfo::from_path(entry.path()) {
                            Ok(info) => info,
                            Err(err) => {
                                panic!("Failed to read '{}' as AssetInfo: {}", entry.path().display(), err);
                            },
                        };
                        infos.push(info.into());
                    }
                }
            } else if entry.path().is_dir() {
                // Recurse, i.e., list and add to the todo list
                let new_dir = match fs::read_dir(entry.path()) {
                    Ok(dir) => dir,
                    Err(err) => {
                        panic!("Failed to list nested tests directory '{}': {}", entry.path().display(), err);
                    },
                };
                if todo.len() == todo.capacity() {
                    todo.reserve(todo.capacity());
                }
                todo.push((entry.path(), new_dir));
            } else {
                // Dunno what to do with it
                println!("Ignoring entry '{}' in '{}' (unknown entry type)", entry.path().display(), path.display());
            }
        }
    }

    // Done
    match DataIndex::from_infos(infos) {
        Ok(index) => index,
        Err(err) => {
            panic!("Failed to create data index from data infos: {}", err);
        },
    }
}

/// Runs a given closure on all files in the `tests` folder (see the constant defined in this function's source file).
///
/// # Generic arguments
/// - `F`: The function signature of the closure. It simply accepts the path and source text of a single file, and returns nothing. Instead, it can panic if the test fails.
///
/// # Arguments
/// - `mode`: The mode to run in. May either be 'BraneScript' or 'Bakery'.
/// - `exec`: The closure that runs code on every file in the appropriate language's text.
///
/// # Panics
/// This function panics if the test failed (i.e., if the files could not be found or the closure panics).
#[inline]
pub fn test_on_dsl_files<F>(mode: &'static str, exec: F)
where
    F: Fn(PathBuf, String),
{
    test_on_dsl_files_in(mode, PathBuf::from(TESTS_DIR), exec)
}
/// Runs a given closure on all files in the given folder.
///
/// # Generic arguments
/// - `F`: The function signature of the closure. It simply accepts the path and source text of a single file, and returns nothing. Instead, it can panic if the test fails.
///
/// # Arguments
/// - `mode`: The mode to run in. May either be 'BraneScript' or 'Bakery'.
/// - `path`: The path to search for files in.
/// - `exec`: The closure that runs code on every file in the appropriate language's text.
///
/// # Panics
/// This function panics if the test failed (i.e., if the files could not be found or the closure panics).
#[inline]
pub fn test_on_dsl_files_in<F>(mode: &'static str, path: impl AsRef<Path>, exec: F)
where
    F: Fn(PathBuf, String),
{
    // Create a runtime on this thread and then do the async version
    let runtime: Runtime = Builder::new_current_thread().build().unwrap_or_else(|err| panic!("Failed to launch Tokio runtime: {}", err));

    // Run the test_on_dsl_files_async
    runtime.block_on(test_on_dsl_files_in_async(mode, path, |path, code| async { exec(path, code) }))
}

/// Runs a given closure on all files in the `tests` folder (see the constant defined in this function's source file).
///
/// This function runs the searching and loading of files asynchronously, for server contexts.
///
/// # Generic arguments
/// - `F`: The function signature of the closure. It simply accepts the path and source text of a single file, and returns a future that represents the test code. If it should cause the test to fail, that future should panic.
///
/// # Arguments
/// - `mode`: The mode to run in. May either be 'BraneScript' or 'Bakery'.
/// - `exec`: The closure that runs code on every file in the appropriate language's text.
///
/// # Panics
/// This function panics if the test failed (i.e., if the files could not be found or the closure panics).
pub async fn test_on_dsl_files_async<F, R>(mode: &'static str, exec: F)
where
    F: Fn(PathBuf, String) -> R,
    R: Future<Output = ()>,
{
    test_on_dsl_files_in_async(mode, PathBuf::from(TESTS_DIR), exec).await
}
/// Runs a given closure on all files in the given folder.
///
/// This function runs the searching and loading of files asynchronously, for server contexts.
///
/// # Generic arguments
/// - `F`: The function signature of the closure. It simply accepts the path and source text of a single file, and returns a future that represents the test code. If it should cause the test to fail, that future should panic.
///
/// # Arguments
/// - `mode`: The mode to run in. May either be 'BraneScript' or 'Bakery'.
/// - `path`: The path to search for files in.
/// - `exec`: The closure that runs code on every file in the appropriate language's text.
///
/// # Panics
/// This function panics if the test failed (i.e., if the files could not be found or the closure panics).
pub async fn test_on_dsl_files_in_async<F, R>(mode: &'static str, path: impl AsRef<Path>, exec: F)
where
    F: Fn(PathBuf, String) -> R,
    R: Future<Output = ()>,
{
    // Setup logger if told
    if std::env::var("TEST_LOGGER").map(|value| value == "1" || value == "true").unwrap_or(false) {
        if let Err(err) = HumanLogger::terminal(DebugMode::Full).init() {
            eprintln!("WARNING: Failed to setup test logger: {err} (no logging for this session)");
        }
    }
    // See if we need to limit ourselves to particular files
    let test_files: Option<Vec<String>> =
        std::env::var("TEST_FILES").ok().map(|test_file| test_file.split(',').map(|test_file| test_file.to_string()).collect());

    // Setup some variables and checks
    let mut path: Cow<Path> = Cow::Borrowed(path.as_ref());
    let exts: Vec<&'static str> = match mode {
        "BraneScript" => {
            path = Cow::Owned(path.join("branescript"));
            vec!["bs", "bscript"]
        },
        "Bakery" => {
            path = Cow::Owned(path.join("bakery"));
            vec!["bakery"]
        },
        val => {
            panic!("Unknown mode '{}'", val);
        },
    };

    // Try to open the folder
    let dir = match fs::read_dir(&path) {
        Ok(dir) => dir,
        Err(err) => {
            panic!("Failed to list tests directory '{}': {}", path.display(), err);
        },
    };

    // Start a 'recursive' process where we run all '*.bscript' files.
    let mut todo: Vec<(PathBuf, ReadDir)> = vec![(path.into(), dir)];
    let mut counter = 0;
    while let Some((path, dir)) = todo.pop() {
        // Iterate through it
        for entry in dir {
            // Attempt to unwrap the entry
            let entry: DirEntry = match entry {
                Ok(entry) => entry,
                Err(err) => {
                    panic!("Failed to read entry in directory '{}': {}", path.display(), err);
                },
            };

            // Check whether it's a directory or not
            let entry_path: PathBuf = entry.path();
            if entry_path.is_file() {
                // Check if it ends with '.bscript';
                if let Some(ext) = entry_path.extension() {
                    if exts.contains(&ext.to_str().unwrap_or("")) {
                        // Skip the file if told
                        if let Some(test_files) = &test_files {
                            // Continue if not all filters match false
                            let mut allowed: bool = false;
                            for test_file in test_files {
                                if entry_path.ends_with(test_file) {
                                    allowed = true;
                                    break;
                                }
                            }
                            if !allowed {
                                continue;
                            }
                        }

                        // Read the file to a buffer
                        let code: String = match fs::read_to_string(&entry_path) {
                            Ok(code) => code,
                            Err(err) => {
                                panic!("Failed to read {} file '{}': {}", mode, entry_path.display(), err);
                            },
                        };

                        // Run the closure on this file
                        exec(entry_path, code).await;
                        counter += 1;
                    } else if entry_path.extension().is_some()
                        && entry_path.extension().unwrap() != "yml"
                        && entry_path.extension().unwrap() != "yaml"
                    {
                        println!(
                            "Ignoring entry '{}' in '{}' (does not have extensions {})",
                            entry_path.display(),
                            path.display(),
                            exts.iter().map(|e| format!("'.{e}'")).collect::<Vec<String>>().join(", ")
                        );
                    }
                } else {
                    println!("Ignoring entry '{}' in '{}' (cannot extract extension)", entry_path.display(), path.display());
                }
            } else if entry_path.is_dir() {
                // Recurse, i.e., list and add to the todo list
                let new_dir = match fs::read_dir(&entry_path) {
                    Ok(dir) => dir,
                    Err(err) => {
                        panic!("Failed to list nested tests directory '{}': {}", entry_path.display(), err);
                    },
                };
                if todo.len() == todo.capacity() {
                    todo.reserve(todo.capacity());
                }
                todo.push((entry_path, new_dir));
            } else {
                // Dunno what to do with it
                println!("Ignoring entry '{}' in '{}' (unknown entry type)", entry_path.display(), path.display());
            }
        }
    }

    // Do a finishing debug print
    if counter == 0 {
        println!("No files to run.");
    } else {
        println!("Tested {counter} files in total");
    }
}





/***** ADDRESS CHECKING *****/
pub fn ensure_http_schema<S>(url: S, secure: bool) -> Result<String, url::ParseError>
where
    S: Into<String>,
{
    let url = url.into();
    let re = Regex::new(r"^https?://.*").unwrap();

    let url = if re.is_match(&url) { url } else { format!("{}://{}", if secure { "https" } else { "http" }, url) };

    // Check if url is valid.
    let _ = Url::parse(&url)?;

    Ok(url)
}



/// Returns whether the given address is an IP address or not.
///
/// The address can already involve paths or an HTTP schema. In that case, only the 'host' part is checked.
///
/// Both IPv4 and IPv6 addresses are matched.
///
/// # Arguments
/// - `address`: The address to check.
///
/// # Returns
/// true if the address is an IP-address, or false otherwise.
pub fn is_ip_addr(address: impl AsRef<str>) -> bool {
    let address: &str = address.as_ref();

    // Attempt to parse with the URL thing
    let url: Url = match Url::parse(address) {
        Ok(url) => url,
        Err(err) => {
            warn!("Given URL '{}' is not a valid URL to begin with: {}", address, err);
            return false;
        },
    };

    // Examine the base
    if let Some(host) = url.host() {
        let res: bool = matches!(host, Host::Ipv4(_) | Host::Ipv6(_));
        debug!("Address '{}' has a{} as hostname", address, if res { "n IP address" } else { " domain" });
        matches!(host, Host::Ipv4(_) | Host::Ipv6(_))
    } else {
        debug!("Address '{}' has no hostname (so also no IP address)", address);
        false
    }
}





/***** TESTS *****/
#[cfg(test)]
mod tests {
    use super::*;

    /// Test some basic HTTP schemas
    #[test]
    fn ensurehttpschema_noschema_added() {
        let url = ensure_http_schema("localhost", true).unwrap();
        assert_eq!(url, "https://localhost");

        let url = ensure_http_schema("localhost", false).unwrap();
        assert_eq!(url, "http://localhost");
    }

    /// Test some more basic HTTP schemas
    #[test]
    fn ensurehttpschema_schema_nothing() {
        let url = ensure_http_schema("http://localhost", true).unwrap();
        assert_eq!(url, "http://localhost");

        let url = ensure_http_schema("https://localhost", false).unwrap();
        assert_eq!(url, "https://localhost");
    }
}