• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Ortham / libloadorder / 9669712859

25 Jun 2024 09:15PM UTC coverage: 91.599% (+4.7%) from 86.942%
9669712859

push

github

Ortham
Use Starfield plugins in Starfield tests

65 of 65 new or added lines in 5 files covered. (100.0%)

163 existing lines in 11 files now uncovered.

7218 of 7880 relevant lines covered (91.6%)

66616.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.94
/ffi/src/handle.rs
1
/*
2
 * This file is part of libloadorder
3
 *
4
 * Copyright (C) 2017 Oliver Hamlet
5
 *
6
 * libloadorder is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * libloadorder is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with libloadorder. If not, see <http://www.gnu.org/licenses/>.
18
 */
19

20
use std::panic::catch_unwind;
21
use std::path::Path;
22
use std::ptr;
23
use std::sync::RwLock;
24

25
use libc::{c_char, c_uint, size_t};
26
use loadorder::GameId;
27
use loadorder::GameSettings;
28
use loadorder::WritableLoadOrder;
29

30
use crate::constants::*;
31
use crate::helpers::{
32
    error, handle_error, to_c_string, to_c_string_array, to_path_buf_vec, to_str,
33
};
34

35
/// A structure that holds all game-specific data used by libloadorder.
36
///
37
/// Used to keep each game's data independent. Abstracts the definition of libloadorder's internal
38
/// state while still providing type safety across the library.
39
#[allow(non_camel_case_types)]
40
pub type lo_game_handle = *mut GameHandle;
41

42
// This type alias is necessary to make cbindgen treat lo_game_handle as a
43
// pointer to an undefined type, rather than an undefined type itself.
44
type GameHandle = RwLock<Box<dyn WritableLoadOrder>>;
45

46
fn map_game_id(game_id: u32) -> Result<GameId, u32> {
6✔
47
    match game_id {
6✔
48
        x if x == LIBLO_GAME_TES3 => Ok(GameId::Morrowind),
6✔
49
        x if x == LIBLO_GAME_TES4 => Ok(GameId::Oblivion),
5✔
50
        x if x == LIBLO_GAME_TES5 => Ok(GameId::Skyrim),
3✔
51
        x if x == LIBLO_GAME_TES5SE => Ok(GameId::SkyrimSE),
×
52
        x if x == LIBLO_GAME_TES5VR => Ok(GameId::SkyrimVR),
×
53
        x if x == LIBLO_GAME_FO3 => Ok(GameId::Fallout3),
×
54
        x if x == LIBLO_GAME_FNV => Ok(GameId::FalloutNV),
×
55
        x if x == LIBLO_GAME_FO4 => Ok(GameId::Fallout4),
×
56
        x if x == LIBLO_GAME_FO4VR => Ok(GameId::Fallout4VR),
×
57
        x if x == LIBLO_GAME_STARFIELD => Ok(GameId::Starfield),
×
UNCOV
58
        _ => Err(LIBLO_ERROR_INVALID_ARGS),
×
59
    }
60
}
6✔
61

62
/// Initialise a new game handle.
63
///
64
/// Creates a handle for a game, which is then used by all load order and active plugin functions.
65
/// If the game uses the textfile-based load order system, this function also checks if the two
66
/// load order files are in sync, provided they both exist.
67
///
68
/// The game ID is one of the `LIBLO_GAME_*` constants.
69
///
70
/// The game path is the directory where the game's executable is found.
71
///
72
/// The local path is the game's local application data folder, found within `%LOCALAPPDATA%` on
73
/// Windows. If running on Windows, the `local_path` can be null, in which case libloadorder will
74
/// look the path up itself. However, on other operating systems, lookup is not possible, and this
75
/// function must be used to provide the necessary path.
76
///
77
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is returned.
78
#[no_mangle]
79
pub unsafe extern "C" fn lo_create_handle(
6✔
80
    handle: *mut lo_game_handle,
6✔
81
    game_id: c_uint,
6✔
82
    game_path: *const c_char,
6✔
83
    local_path: *const c_char,
6✔
84
) -> c_uint {
6✔
85
    catch_unwind(|| {
6✔
86
        if handle.is_null() || game_path.is_null() {
6✔
87
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer(s) passed");
×
88
        }
6✔
89

90
        let game_id = match map_game_id(game_id) {
6✔
91
            Ok(x) => x,
6✔
UNCOV
92
            Err(x) => return error(x, "Invalid game specified"),
×
93
        };
94

95
        let game_path = match to_str(game_path) {
6✔
96
            Ok(x) => Path::new(x),
6✔
UNCOV
97
            Err(x) => return x,
×
98
        };
99

100
        if !game_path.is_dir() {
6✔
101
            return error(
×
102
                LIBLO_ERROR_INVALID_ARGS,
×
103
                &format!(
×
104
                    "Given game path \"{:?}\" is not a valid directory",
×
105
                    game_path
×
106
                ),
×
107
            );
×
108
        }
6✔
109

6✔
110
        let load_order: Box<dyn WritableLoadOrder>;
6✔
111
        if local_path.is_null() {
6✔
112
            match GameSettings::new(game_id, game_path) {
2✔
113
                Ok(x) => load_order = x.into_load_order(),
1✔
114
                Err(x) => return handle_error(x),
1✔
115
            }
116
        } else {
117
            let local_path = match to_str(local_path) {
4✔
118
                Ok(x) => Path::new(x),
4✔
UNCOV
119
                Err(x) => return x,
×
120
            };
121

122
            if local_path.exists() && !local_path.is_dir() {
4✔
123
                return error(
1✔
124
                    LIBLO_ERROR_INVALID_ARGS,
1✔
125
                    &format!(
1✔
126
                        "Given local data path \"{:?}\" exists but is not a valid directory",
1✔
127
                        local_path
1✔
128
                    ),
1✔
129
                );
1✔
130
            }
3✔
131

3✔
132
            match GameSettings::with_local_path(game_id, game_path, local_path) {
3✔
133
                Ok(x) => load_order = x.into_load_order(),
3✔
134
                Err(x) => return handle_error(x),
×
135
            }
136
        }
137

138
        let is_self_consistent = load_order.is_self_consistent();
4✔
139

4✔
140
        *handle = Box::into_raw(Box::new(RwLock::new(load_order)));
4✔
141

4✔
142
        match is_self_consistent {
4✔
143
            Ok(true) => LIBLO_OK,
4✔
144
            Ok(false) => LIBLO_WARN_LO_MISMATCH,
×
145
            Err(x) => handle_error(x),
×
146
        }
147
    })
6✔
148
    .unwrap_or(LIBLO_ERROR_PANICKED)
6✔
149
}
6✔
150

151
/// Destroy an existing game handle.
152
///
153
/// Destroys the given game handle, freeing up memory allocated during its use, excluding any
154
/// memory allocated to error messages.
155
#[no_mangle]
156
pub unsafe extern "C" fn lo_destroy_handle(handle: lo_game_handle) {
6✔
157
    if !handle.is_null() {
6✔
158
        drop(Box::from_raw(handle));
4✔
159
    }
4✔
160
}
6✔
161

162
/// Load the current load order state, discarding any previously held state.
163
///
164
/// This function should be called whenever the load order or active state of plugins "on disk"
165
/// changes, so that cached state is updated to reflect the changes.
166
#[no_mangle]
167
pub unsafe extern "C" fn lo_load_current_state(handle: lo_game_handle) -> c_uint {
1✔
168
    catch_unwind(|| {
1✔
169
        if handle.is_null() {
1✔
UNCOV
170
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
171
        }
1✔
172

173
        let mut handle = match (*handle).write() {
1✔
174
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
175
            Ok(h) => h,
1✔
176
        };
177

178
        if let Err(x) = handle
1✔
179
            .game_settings_mut()
1✔
180
            .refresh_implicitly_active_plugins()
1✔
181
        {
UNCOV
182
            return handle_error(x);
×
183
        }
1✔
184

185
        if let Err(x) = handle.load() {
1✔
186
            return handle_error(x);
×
187
        }
1✔
188

1✔
189
        LIBLO_OK
1✔
190
    })
1✔
191
    .unwrap_or(LIBLO_ERROR_PANICKED)
1✔
192
}
1✔
193

194
/// Check if the load order is ambiguous, by checking that all plugins in the current load order
195
/// state have a well-defined position in the "on disk" state, and that all data sources are
196
/// consistent. If the load order is ambiguous, different applications may read different load
197
/// orders from the same source data.
198
///
199
/// Outputs `true` if the load order state is ambiguous, and false otherwise.
200
///
201
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is returned.
202
#[no_mangle]
203
pub unsafe extern "C" fn lo_is_ambiguous(handle: lo_game_handle, result: *mut bool) -> c_uint {
×
204
    catch_unwind(|| {
×
205
        if handle.is_null() || result.is_null() {
×
206
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
UNCOV
207
        }
×
208

209
        let handle = match (*handle).read() {
×
210
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
211
            Ok(h) => h,
×
212
        };
×
213

×
214
        match handle.is_ambiguous() {
×
215
            Ok(x) => *result = x,
×
UNCOV
216
            Err(x) => return handle_error(x),
×
217
        }
218

219
        LIBLO_OK
×
220
    })
×
221
    .unwrap_or(LIBLO_ERROR_PANICKED)
×
UNCOV
222
}
×
223

224
/// Fix up the text file(s) used by the load order and active plugins systems.
225
///
226
/// This checks that the load order and active plugin lists conform to libloadorder's validity
227
/// requirements (see Miscellaneous Details for details), and resolves any issues encountered, then
228
/// saves the fixed lists.
229
///
230
/// For the case of a plugin appearing multiple times in a load order / active plugin list,
231
/// libloadorder discards all but the last instance of that plugin.
232
///
233
/// This can be useful for when plugins are uninstalled manually or by a utility that does not also
234
/// update the load order / active plugins systems correctly.
235
///
236
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is returned.
237
#[no_mangle]
238
pub unsafe extern "C" fn lo_fix_plugin_lists(handle: lo_game_handle) -> c_uint {
×
239
    catch_unwind(|| {
×
240
        if handle.is_null() {
×
241
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
UNCOV
242
        }
×
243

244
        let mut handle = match (*handle).write() {
×
245
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
UNCOV
246
            Ok(h) => h,
×
247
        };
248

249
        if let Err(x) = handle.load() {
×
250
            return handle_error(x);
×
UNCOV
251
        }
×
252

253
        if let Err(x) = handle.save() {
×
254
            return handle_error(x);
×
255
        }
×
256

×
257
        LIBLO_OK
×
258
    })
×
259
    .unwrap_or(LIBLO_ERROR_PANICKED)
×
UNCOV
260
}
×
261

262
/// Get the list of implicitly active plugins for the given handle's game.
263
///
264
/// The list may be empty if the game has no implicitly active plugins. The list
265
/// may include plugins that are not installed. Plugins are not necessarily
266
/// listed in their load order.
267
///
268
/// If the list is empty, the `plugins` pointer will be null and `num_plugins`
269
/// will be `0`.
270
///
271
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is
272
/// returned.
273
#[no_mangle]
274
pub unsafe extern "C" fn lo_get_implicitly_active_plugins(
2✔
275
    handle: lo_game_handle,
2✔
276
    plugins: *mut *mut *mut c_char,
2✔
277
    num_plugins: *mut size_t,
2✔
278
) -> c_uint {
2✔
279
    catch_unwind(|| {
2✔
280
        if handle.is_null() || plugins.is_null() || num_plugins.is_null() {
2✔
281
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
282
        }
2✔
283

284
        let handle = match (*handle).read() {
2✔
285
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
286
            Ok(h) => h,
2✔
287
        };
2✔
288

2✔
289
        *plugins = ptr::null_mut();
2✔
290
        *num_plugins = 0;
2✔
291

2✔
292
        let plugin_names = handle.game_settings().implicitly_active_plugins();
2✔
293

2✔
294
        if plugin_names.is_empty() {
2✔
295
            return LIBLO_OK;
1✔
296
        }
1✔
297

1✔
298
        match to_c_string_array(plugin_names) {
1✔
299
            Ok((pointer, size)) => {
1✔
300
                *plugins = pointer;
1✔
301
                *num_plugins = size;
1✔
302
            }
1✔
303
            Err(x) => return error(x, "A filename contained a null byte"),
×
304
        }
305

306
        LIBLO_OK
1✔
307
    })
2✔
308
    .unwrap_or(LIBLO_ERROR_PANICKED)
2✔
309
}
2✔
310

311
/// Get the list of implicitly active plugins for the given handle's game.
312
///
313
/// The list may be empty if the game has no early loading plugins. The list
314
/// may include plugins that are not installed. Plugins are listed in their
315
/// hardcoded load order.
316
///
317
/// Note that for the original Skyrim, `Update.esm` is hardcoded to always load,
318
/// but not in a specific location, unlike all other early loading plugins
319
/// for all games, which must load in the given order, before any other plugins.
320
///
321
/// The order of Creation Club plugins as listed in `Fallout4.ccc` or
322
/// `Skyrim.ccc` is as their hardcoded load order for libloadorder's purposes.
323
///
324
/// If the list is empty, the `plugins` pointer will be null and `num_plugins`
325
/// will be `0`.
326
///
327
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is
328
/// returned.
329
#[no_mangle]
330
pub unsafe extern "C" fn lo_get_early_loading_plugins(
×
331
    handle: lo_game_handle,
×
332
    plugins: *mut *mut *mut c_char,
×
333
    num_plugins: *mut size_t,
×
UNCOV
334
) -> c_uint {
×
335
    catch_unwind(|| {
×
336
        if handle.is_null() || plugins.is_null() || num_plugins.is_null() {
×
337
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
UNCOV
338
        }
×
339

340
        let handle = match (*handle).read() {
×
341
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
UNCOV
342
            Ok(h) => h,
×
343
        };
×
344

×
345
        *plugins = ptr::null_mut();
×
346
        *num_plugins = 0;
×
UNCOV
347

×
UNCOV
348
        let plugin_names = handle.game_settings().early_loading_plugins();
×
UNCOV
349

×
350
        if plugin_names.is_empty() {
×
351
            return LIBLO_OK;
×
352
        }
×
UNCOV
353

×
UNCOV
354
        match to_c_string_array(plugin_names) {
×
355
            Ok((pointer, size)) => {
×
356
                *plugins = pointer;
×
357
                *num_plugins = size;
×
358
            }
×
UNCOV
359
            Err(x) => return error(x, "A filename contained a null byte"),
×
360
        }
361

UNCOV
362
        LIBLO_OK
×
UNCOV
363
    })
×
UNCOV
364
    .unwrap_or(LIBLO_ERROR_PANICKED)
×
UNCOV
365
}
×
366

367
/// Get the active plugins file path for the given game handle.
368
///
369
/// The active plugins file path is often within the game's local path, but its
370
/// name and location varies by game and game configuration, so this function
371
/// exposes the path that libloadorder chooses to use.
372
///
373
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is
374
/// returned.
375
#[no_mangle]
376
pub unsafe extern "C" fn lo_get_active_plugins_file_path(
×
377
    handle: lo_game_handle,
×
378
    path: *mut *mut c_char,
×
379
) -> c_uint {
×
UNCOV
380
    catch_unwind(|| {
×
381
        if handle.is_null() || path.is_null() {
×
382
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
383
        }
×
384

UNCOV
385
        let handle = match (*handle).read() {
×
386
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
387
            Ok(h) => h,
×
388
        };
389

UNCOV
390
        let file_path = match handle.game_settings().active_plugins_file().to_str() {
×
391
            Some(p) => p,
×
392
            None => {
393
                return error(
×
394
                    LIBLO_ERROR_PATH_ENCODE_FAIL,
×
395
                    "The active plugins file path could not be encoded in UTF-8",
×
396
                )
×
397
            }
398
        };
399

UNCOV
400
        match to_c_string(file_path) {
×
UNCOV
401
            Ok(x) => *path = x,
×
UNCOV
402
            Err(x) => return error(x, "The filename contained a null byte"),
×
403
        }
404

UNCOV
405
        LIBLO_OK
×
UNCOV
406
    })
×
UNCOV
407
    .unwrap_or(LIBLO_ERROR_PANICKED)
×
UNCOV
408
}
×
409

410
/// Sets the additional plugins directories to be recognised by the given handle.
411
///
412
/// If the load order contains plugins that are installed outside of the game's plugins directory,
413
/// this function can be used to provide the paths to the directories that those plugins are in so that libloadorder is able to
414
/// find them.
415
///
416
/// If external plugins exist, this function must be called before performing any operations on
417
/// the load order to avoid any unexpected behaviour.
418
///
419
/// Returns `LIBLO_OK` if successful, otherwise a `LIBLO_ERROR_*` code is returned.
420
#[no_mangle]
UNCOV
421
pub unsafe extern "C" fn lo_set_additional_plugins_directories(
×
UNCOV
422
    handle: lo_game_handle,
×
UNCOV
423
    paths: *const *const c_char,
×
UNCOV
424
    num_paths: size_t,
×
UNCOV
425
) -> c_uint {
×
UNCOV
426
    catch_unwind(|| {
×
UNCOV
427
        if handle.is_null() || (paths.is_null() && num_paths != 0) {
×
UNCOV
428
            return error(LIBLO_ERROR_INVALID_ARGS, "Null pointer passed");
×
UNCOV
429
        }
×
430

UNCOV
431
        let mut handle = match (*handle).write() {
×
UNCOV
432
            Err(e) => return error(LIBLO_ERROR_POISONED_THREAD_LOCK, &e.to_string()),
×
UNCOV
433
            Ok(h) => h,
×
434
        };
435

UNCOV
436
        let plugin_paths = match to_path_buf_vec(paths, num_paths) {
×
UNCOV
437
            Ok(x) => x,
×
UNCOV
438
            Err(x) => return error(x, "A plugin path was null or not valid UTF-8"),
×
439
        };
440

UNCOV
441
        handle
×
UNCOV
442
            .game_settings_mut()
×
UNCOV
443
            .set_additional_plugins_directories(plugin_paths);
×
UNCOV
444

×
UNCOV
445
        LIBLO_OK
×
UNCOV
446
    })
×
UNCOV
447
    .unwrap_or(LIBLO_ERROR_PANICKED)
×
UNCOV
448
}
×
449

450
#[cfg(test)]
451
mod tests {
452
    use std::ffi::CString;
453
    use std::path::PathBuf;
454

455
    use super::*;
456

457
    #[test]
458
    fn lo_create_handle_should_allow_a_non_existent_local_path() {
1✔
459
        let mut handle: lo_game_handle = std::ptr::null_mut();
1✔
460
        let game_path = CString::new(".").unwrap();
1✔
461
        let local_path = CString::new("does-not-exist").unwrap();
1✔
462

1✔
463
        assert!(!PathBuf::from(local_path.to_string_lossy().to_string()).exists());
1✔
464

465
        unsafe {
466
            let result = lo_create_handle(
1✔
467
                &mut handle,
1✔
468
                LIBLO_GAME_TES5,
1✔
469
                game_path.as_ptr(),
1✔
470
                local_path.as_ptr(),
1✔
471
            );
1✔
472
            lo_destroy_handle(handle);
1✔
473

1✔
474
            assert_eq!(LIBLO_OK, result);
1✔
475
        }
476
    }
1✔
477

478
    #[test]
479
    fn lo_create_handle_should_allow_a_directory_local_path_that_exists() {
1✔
480
        let mut handle: lo_game_handle = std::ptr::null_mut();
1✔
481
        let game_path = CString::new(".").unwrap();
1✔
482
        let local_path = CString::new(".").unwrap();
1✔
483

1✔
484
        unsafe {
1✔
485
            let result = lo_create_handle(
1✔
486
                &mut handle,
1✔
487
                LIBLO_GAME_TES5,
1✔
488
                game_path.as_ptr(),
1✔
489
                local_path.as_ptr(),
1✔
490
            );
1✔
491
            lo_destroy_handle(handle);
1✔
492

1✔
493
            assert_eq!(LIBLO_OK, result);
1✔
494
        }
495
    }
1✔
496

497
    #[test]
498
    fn lo_create_handle_should_error_if_given_a_non_directory_local_path_that_exists() {
1✔
499
        let mut handle: lo_game_handle = std::ptr::null_mut();
1✔
500
        let game_path = CString::new(".").unwrap();
1✔
501
        let local_path = CString::new(
1✔
502
            std::env::current_exe()
1✔
503
                .unwrap()
1✔
504
                .to_string_lossy()
1✔
505
                .as_bytes(),
1✔
506
        )
1✔
507
        .unwrap();
1✔
508

1✔
509
        let local_path_buf = PathBuf::from(local_path.to_string_lossy().to_string());
1✔
510

1✔
511
        assert!(local_path_buf.exists());
1✔
512
        assert!(!local_path_buf.is_dir());
1✔
513

514
        unsafe {
515
            let result = lo_create_handle(
1✔
516
                &mut handle,
1✔
517
                LIBLO_GAME_TES5,
1✔
518
                game_path.as_ptr(),
1✔
519
                local_path.as_ptr(),
1✔
520
            );
1✔
521
            lo_destroy_handle(handle);
1✔
522

1✔
523
            assert_eq!(LIBLO_ERROR_INVALID_ARGS, result);
1✔
524
        }
525
    }
1✔
526

527
    #[test]
528
    fn lo_create_handle_should_always_accept_a_null_local_path_if_game_is_morrowind() {
1✔
529
        let mut handle: lo_game_handle = std::ptr::null_mut();
1✔
530
        let game_path = CString::new(".").unwrap();
1✔
531

1✔
532
        unsafe {
1✔
533
            let result = lo_create_handle(
1✔
534
                &mut handle,
1✔
535
                LIBLO_GAME_TES3,
1✔
536
                game_path.as_ptr(),
1✔
537
                std::ptr::null(),
1✔
538
            );
1✔
539
            lo_destroy_handle(handle);
1✔
540

1✔
541
            assert_eq!(LIBLO_OK, result);
1✔
542
        }
543
    }
1✔
544

545
    #[test]
546
    #[cfg(windows)]
547
    fn lo_create_handle_should_accept_a_null_local_path_if_game_is_not_morrowind() {
548
        let mut handle: lo_game_handle = std::ptr::null_mut();
549
        let game_path = CString::new(".").unwrap();
550

551
        unsafe {
552
            let result = lo_create_handle(
553
                &mut handle,
554
                LIBLO_GAME_TES4,
555
                game_path.as_ptr(),
556
                std::ptr::null(),
557
            );
558
            lo_destroy_handle(handle);
559

560
            assert_eq!(LIBLO_OK, result);
561
        }
562
    }
563

564
    #[test]
565
    #[cfg(not(windows))]
566
    fn lo_create_handle_should_not_accept_a_null_local_path_if_game_is_not_morrowind() {
1✔
567
        let mut handle: lo_game_handle = std::ptr::null_mut();
1✔
568
        let game_path = CString::new(".").unwrap();
1✔
569

1✔
570
        unsafe {
1✔
571
            let result = lo_create_handle(
1✔
572
                &mut handle,
1✔
573
                LIBLO_GAME_TES4,
1✔
574
                game_path.as_ptr(),
1✔
575
                std::ptr::null(),
1✔
576
            );
1✔
577
            lo_destroy_handle(handle);
1✔
578

1✔
579
            assert_eq!(LIBLO_ERROR_INVALID_ARGS, result);
1✔
580
        }
581
    }
1✔
582

583
    #[test]
584
    #[cfg(not(windows))]
585
    fn lo_load_current_state_should_refresh_implicitly_active_plugins() {
1✔
586
        let tmp_dir = tempfile::tempdir().unwrap();
1✔
587
        let game_path = tmp_dir.path();
1✔
588
        let local_path = game_path.join("AppData/Local/Oblivion");
1✔
589
        let ini_path = game_path.join("Documents/My Games/Oblivion/Oblivion.ini");
1✔
590

1✔
591
        let mut handle: lo_game_handle = std::ptr::null_mut();
1✔
592

1✔
593
        let game_path = CString::new(game_path.to_str().unwrap()).unwrap();
1✔
594
        let local_path = CString::new(local_path.to_str().unwrap()).unwrap();
1✔
595

1✔
596
        unsafe {
1✔
597
            let result = lo_create_handle(
1✔
598
                &mut handle,
1✔
599
                LIBLO_GAME_TES4,
1✔
600
                game_path.as_ptr(),
1✔
601
                local_path.as_ptr(),
1✔
602
            );
1✔
603
            assert_eq!(LIBLO_OK, result);
1✔
604
        }
605

606
        unsafe {
607
            let mut plugins: *mut *mut c_char = std::ptr::null_mut();
1✔
608
            let mut num_plugins: size_t = 0;
1✔
609

1✔
610
            let result = lo_get_implicitly_active_plugins(handle, &mut plugins, &mut num_plugins);
1✔
611
            assert_eq!(LIBLO_OK, result);
1✔
612
            assert_eq!(0, num_plugins);
1✔
613

614
            crate::lo_free_string_array(plugins, num_plugins);
1✔
615
        }
1✔
616

1✔
617
        std::fs::create_dir_all(&ini_path.parent().unwrap()).unwrap();
1✔
618
        std::fs::write(&ini_path, "[General]\nsTestFile1=Blank.esp").unwrap();
1✔
619

1✔
620
        unsafe {
1✔
621
            let result = lo_load_current_state(handle);
1✔
622
            assert_eq!(LIBLO_OK, result);
1✔
623
        }
624

625
        unsafe {
626
            let mut plugins: *mut *mut c_char = std::ptr::null_mut();
1✔
627
            let mut num_plugins: size_t = 0;
1✔
628

1✔
629
            let result = lo_get_implicitly_active_plugins(handle, &mut plugins, &mut num_plugins);
1✔
630
            assert_eq!(LIBLO_OK, result);
1✔
631
            assert_eq!(1, num_plugins);
1✔
632

633
            crate::lo_free_string_array(plugins, num_plugins);
1✔
634
        }
1✔
635

1✔
636
        unsafe {
1✔
637
            lo_destroy_handle(handle);
1✔
638
        }
1✔
639
    }
1✔
640
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc