Skip to content

Commit

Permalink
Add more validation for blueprint plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
Ortham committed Aug 23, 2024
1 parent ac75443 commit d98cf27
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 1 deletion.
1 change: 1 addition & 0 deletions ffi/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ fn map_error(err: &Error) -> c_uint {
IniParsingError { .. } => LIBLO_ERROR_FILE_PARSE_FAIL,
VdfParsingError(_, _) => LIBLO_ERROR_FILE_PARSE_FAIL,
SystemError(_, _) => LIBLO_ERROR_SYSTEM_ERROR,
InvalidBlueprintPluginPosition { .. } => LIBLO_ERROR_INVALID_ARGS,
_ => LIBLO_ERROR_INTERNAL_LOGIC_ERROR,
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ pub enum Error {
},
VdfParsingError(PathBuf, String),
SystemError(i32, OsString),
InvalidBlueprintPluginPosition {
name: String,
pos: usize,
expected_pos: usize,
},
}

#[cfg(windows)]
Expand Down Expand Up @@ -166,6 +171,8 @@ impl fmt::Display for Error {
write!(f, "Failed to parse VDF file at {path:?}: {message}"),
Error::SystemError(code, message) =>
write!(f, "Error returned by the operating system, code {code}: {message:?}"),
Error::InvalidBlueprintPluginPosition{ name, pos, expected_pos } =>
write!(f, "Attempted to load the blueprint plugin \"{name}\" at position {pos}, its expected position is {expected_pos}"),
}
}
}
Expand Down
106 changes: 105 additions & 1 deletion src/load_order/mutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,26 @@ fn validate_blueprint_plugin_index(
}
}

Ok(())
let following_plugins = if index < plugins.len() {
&plugins[index..]
} else {
&[]
};

// Check that all of the following plugins are blueprint plugins.
let last_non_blueprint_pos = following_plugins
.iter()
.rposition(|p| !p.is_blueprint_master())
.map(|i| index + i);

match last_non_blueprint_pos {
Some(i) => Err(Error::InvalidBlueprintPluginPosition {
name: plugin.name().to_string(),
pos: index,
expected_pos: i + 1,
}),
_ => Ok(()),
}
}

fn validate_master_file_index(
Expand Down Expand Up @@ -647,6 +666,8 @@ fn validate_load_order(plugins: &[Plugin], early_loading_plugins: &[String]) ->

validate_no_unhoisted_non_masters_before_masters(plugins)?;

validate_no_non_blueprint_plugins_after_blueprint_plugins(plugins)?;

validate_plugins_load_before_their_masters(plugins)?;

Ok(())
Expand Down Expand Up @@ -699,6 +720,31 @@ fn validate_no_unhoisted_non_masters_before_masters(plugins: &[Plugin]) -> Resul
Ok(())
}

fn validate_no_non_blueprint_plugins_after_blueprint_plugins(
plugins: &[Plugin],
) -> Result<(), Error> {
let first_blueprint_plugin = plugins
.iter()
.enumerate()
.find(|(_, p)| p.is_blueprint_master());

if let Some((first_blueprint_pos, first_blueprint_plugin)) = first_blueprint_plugin {
let last_non_blueprint_pos = plugins.iter().rposition(|p| !p.is_blueprint_master());

if let Some(last_non_blueprint_pos) = last_non_blueprint_pos {
if last_non_blueprint_pos > first_blueprint_pos {
return Err(Error::InvalidBlueprintPluginPosition {
name: first_blueprint_plugin.name().to_string(),
pos: first_blueprint_pos,
expected_pos: last_non_blueprint_pos,
});
}
}
}

Ok(())
}

fn validate_plugins_load_before_their_masters(plugins: &[Plugin]) -> Result<(), Error> {
let mut plugins_map: HashMap<UniCase<String>, &Plugin> = HashMap::new();

Expand Down Expand Up @@ -1369,6 +1415,34 @@ mod tests {
assert!(load_order.validate_index(&plugin, 2).is_ok());
}

#[test]
fn validate_index_should_fail_for_a_blueprint_plugin_index_if_any_non_blueprint_plugins_follow_it(
) {
let tmp_dir = tempdir().unwrap();
let load_order = prepare(GameId::Starfield, &tmp_dir.path());

let plugins_dir = load_order.game_settings().plugins_directory();

let plugin_name = "Blank.full.esm";
set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();

let plugin = Plugin::new(plugin_name, load_order.game_settings()).unwrap();

let index = 1;
match load_order.validate_index(&plugin, index).unwrap_err() {
Error::InvalidBlueprintPluginPosition {
name,
pos,
expected_pos,
} => {
assert_eq!(plugin_name, name);
assert_eq!(index, pos);
assert_eq!(2, expected_pos);
}
e => panic!("Unexpected error type: {:?}", e),
}
}

#[test]
fn validate_index_should_fail_for_a_blueprint_plugin_index_that_is_after_a_dependent_blueprint_plugin_index(
) {
Expand Down Expand Up @@ -2356,6 +2430,36 @@ mod tests {
assert!(validate_load_order(&plugins, &[]).is_ok());
}

#[test]
fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_before_a_non_blueprint_plugin() {
let tmp_dir = tempdir().unwrap();
let settings = prepare(GameId::Starfield, &tmp_dir.path()).game_settings;

let plugins_dir = settings.plugins_directory();

let plugin_name = "Blank.full.esm";
set_blueprint_flag(GameId::Starfield, &plugins_dir.join(plugin_name), true).unwrap();

let plugins = vec![
Plugin::new("Starfield.esm", &settings).unwrap(),
Plugin::new(plugin_name, &settings).unwrap(),
Plugin::new("Blank.esp", &settings).unwrap(),
];

match validate_load_order(&plugins, &[]).unwrap_err() {
Error::InvalidBlueprintPluginPosition {
name,
pos,
expected_pos,
} => {
assert_eq!(plugin_name, name);
assert_eq!(1, pos);
assert_eq!(2, expected_pos);
}
e => panic!("Unexpected error type: {:?}", e),
}
}

#[test]
fn validate_load_order_should_fail_if_a_blueprint_plugin_loads_after_a_blueprint_plugin_that_depends_on_it(
) {
Expand Down

0 comments on commit d98cf27

Please sign in to comment.