-
Notifications
You must be signed in to change notification settings - Fork 73
feat: implement export filter functionality for resource exports #1621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Gijsreyn
wants to merge
31
commits into
PowerShell:main
Choose a base branch
from
Gijsreyn:gh-1486/main/add-postfilter-export
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
aeb3c44
feat: implement export filter functionality for resource exports
Gijsreyn ba1baec
Remove comment
Gijsreyn 00ec8fb
feat: implement export filter functionality for resource exports
Gijsreyn 664abad
Remove comment
Gijsreyn 87a8f3a
Fix Copilot remarks
Gijsreyn e992aaa
Merge branch 'gh-1486/main/add-postfilter-export' of https://github.c…
Gijsreyn ca50052
Wrong commit
Gijsreyn f7f74f8
Merge branch 'main' into gh-1486/main/add-postfilter-export
Gijsreyn 6c176a1
Remove the wildcard support for resources
Gijsreyn 0f5bfd4
Merge branch 'main' into gh-1486/main/add-postfilter-export
Gijsreyn 94d221b
Fix Copilot remarks
Gijsreyn 9e062ba
Merge branch 'main' into gh-1486/main/add-postfilter-export
Gijsreyn b37cab7
Attempt to increase code coverage and fix test
Gijsreyn f86d09c
Add additional test for coverage
Gijsreyn bd20f48
Add test and fix dism_dsc
Gijsreyn e389025
fix: correct code coverage calculation for uninstrumented files
SteveL-MSFT e2e005e
Merge remote-tracking branch 'upstream/main' into gh-1486/main/add-po…
Gijsreyn 72aa474
Refactor work on services
Gijsreyn 46be9a2
Merge branch 'gh-1486/main/add-postfilter-export' of https://github.c…
Gijsreyn 1bea448
Remove unused key
Gijsreyn 40dacf0
Revert change
Gijsreyn 17a0a88
Restore native resource filtering and add engine filtering fallback
Gijsreyn 537af5c
Merge remote-tracking branch 'upstream/main' into gh-1486/main/add-po…
Gijsreyn be7612b
Update resource definitions
Gijsreyn 01eff16
Fix Copilot remark
Gijsreyn 1833d8c
remove directive
Gijsreyn 38c3de9
Merge branch 'main' into gh-1486/main/add-postfilter-export
Gijsreyn 4e5daed
Merge branch 'main' into gh-1486/main/add-postfilter-export
Gijsreyn 3aa2369
Change comment wording and update tests
Gijsreyn 5662929
Merge branch 'main' of https://github.com/Gijsreyn/operation-methods …
Gijsreyn ccf8df8
Add crate
Gijsreyn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use rust_i18n::t; | ||
| use serde_json::{Map, Value}; | ||
| use tracing::debug; | ||
|
|
||
| /// Apply an export filter to a list of exported instances, retaining only matching instances. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `instances` - The exported instances to filter. | ||
| /// * `filters` - The filter objects from the `exportFilter` directive. | ||
| pub fn apply_export_filter(instances: &mut Vec<Value>, filters: &[Map<String, Value>]) { | ||
| if filters.is_empty() { | ||
| // an empty filter list means no filtering is applied | ||
| return; | ||
| } | ||
|
|
||
| let original_count = instances.len(); | ||
| instances.retain(|instance| instance_matches_filters(instance, filters)); | ||
| debug!("{}", t!("configure.export_filter.filteredInstances", original = original_count, retained = instances.len())); | ||
| } | ||
|
|
||
| /// Check if an instance matches any of the filter objects (logical OR). | ||
| #[must_use] | ||
| pub fn instance_matches_filters(instance: &Value, filters: &[Map<String, Value>]) -> bool { | ||
|
Gijsreyn marked this conversation as resolved.
Outdated
|
||
| let Some(instance) = instance.as_object() else { | ||
| // non-object instances can't be matched by property filters | ||
| return false; | ||
| }; | ||
|
|
||
| filters.iter().any(|filter| instance_matches_filter(instance, filter)) | ||
| } | ||
|
|
||
| /// Check if an instance matches all properties of a single filter object (logical AND). | ||
| fn instance_matches_filter(instance: &Map<String, Value>, filter: &Map<String, Value>) -> bool { | ||
| filter.iter().all(|(name, expected)| { | ||
| instance.get(name).is_some_and(|actual| value_matches(actual, expected)) | ||
| }) | ||
| } | ||
|
|
||
| /// Check if an actual value matches an expected filter value. | ||
| fn value_matches(actual: &Value, expected: &Value) -> bool { | ||
| match (actual, expected) { | ||
| // strings are compared case-insensitively with `*` wildcard support | ||
| (Value::String(actual_str), Value::String(pattern)) => wildcard_match(pattern, actual_str), | ||
| // nested objects match recursively as a partial match | ||
| (Value::Object(actual_obj), Value::Object(expected_obj)) => instance_matches_filter(actual_obj, expected_obj), | ||
| // everything else requires equality | ||
| _ => actual == expected, | ||
| } | ||
| } | ||
|
|
||
| /// Match `text` against `pattern` where `*` matches zero or more characters. | ||
| /// The comparison is case-insensitive. | ||
| fn wildcard_match(pattern: &str, text: &str) -> bool { | ||
| let pattern: Vec<char> = pattern.to_lowercase().chars().collect(); | ||
| let text: Vec<char> = text.to_lowercase().chars().collect(); | ||
|
|
||
| // iterative greedy matching with backtracking on the last `*` | ||
| let (mut p, mut t) = (0usize, 0usize); | ||
| let mut star: Option<usize> = None; | ||
| let mut star_text = 0usize; | ||
|
|
||
| while t < text.len() { | ||
| if p < pattern.len() && pattern[p] == '*' { | ||
| star = Some(p); | ||
| star_text = t; | ||
| p += 1; | ||
| } else if p < pattern.len() && pattern[p] == text[t] { | ||
| p += 1; | ||
| t += 1; | ||
| } else if let Some(star_pos) = star { | ||
| // backtrack: let the last `*` consume one more character | ||
| p = star_pos + 1; | ||
| star_text += 1; | ||
| t = star_text; | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // remaining pattern must be all `*` | ||
| pattern[p..].iter().all(|c| *c == '*') | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use serde_json::json; | ||
|
|
||
| fn to_filters(value: Value) -> Vec<Map<String, Value>> { | ||
| serde_json::from_value(value).unwrap() | ||
| } | ||
|
|
||
| #[test] | ||
| fn wildcard_match_exact() { | ||
| assert!(wildcard_match("sshd", "sshd")); | ||
| assert!(!wildcard_match("sshd", "sshd2")); | ||
| assert!(!wildcard_match("sshd2", "sshd")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn wildcard_match_case_insensitive() { | ||
| assert!(wildcard_match("SSHD", "sshd")); | ||
| assert!(wildcard_match("*Ssh*", "OpenSSH Server")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn wildcard_match_star() { | ||
| assert!(wildcard_match("*ssh*", "ssh")); | ||
| assert!(wildcard_match("*ssh*", "openssh-server")); | ||
| assert!(wildcard_match("ssh*", "sshd")); | ||
| assert!(wildcard_match("*shd", "sshd")); | ||
| assert!(wildcard_match("*", "")); | ||
| assert!(wildcard_match("*", "anything")); | ||
| assert!(wildcard_match("s*h*d", "sshd")); | ||
|
Gijsreyn marked this conversation as resolved.
|
||
| assert!(!wildcard_match("*ssh*", "no match")); | ||
| assert!(!wildcard_match("ssh*", "openssh")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn empty_filter_list_matches_nothing_but_apply_is_noop() { | ||
| let mut instances = vec![json!({"name": "one"}), json!({"name": "two"})]; | ||
| apply_export_filter(&mut instances, &[]); | ||
| assert_eq!(instances.len(), 2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn filters_are_logical_or() { | ||
| let filters = to_filters(json!([ | ||
| { "name": "*ssh*" }, | ||
| { "startType": "automatic" } | ||
| ])); | ||
| // matches first filter | ||
| assert!(instance_matches_filters(&json!({"name": "sshd", "startType": "manual"}), &filters)); | ||
| // matches second filter | ||
| assert!(instance_matches_filters(&json!({"name": "spooler", "startType": "automatic"}), &filters)); | ||
| // matches neither | ||
| assert!(!instance_matches_filters(&json!({"name": "spooler", "startType": "manual"}), &filters)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn properties_within_filter_are_logical_and() { | ||
| let filters = to_filters(json!([ | ||
| { "name": "*ssh*", "startType": "automatic" } | ||
| ])); | ||
| assert!(instance_matches_filters(&json!({"name": "sshd", "startType": "automatic"}), &filters)); | ||
| assert!(!instance_matches_filters(&json!({"name": "sshd", "startType": "manual"}), &filters)); | ||
| assert!(!instance_matches_filters(&json!({"name": "spooler", "startType": "automatic"}), &filters)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn missing_property_does_not_match() { | ||
| let filters = to_filters(json!([{ "name": "*ssh*" }])); | ||
| assert!(!instance_matches_filters(&json!({"startType": "automatic"}), &filters)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn non_string_values_use_equality() { | ||
| let filters = to_filters(json!([{ "count": 2, "enabled": true }])); | ||
| assert!(instance_matches_filters(&json!({"count": 2, "enabled": true}), &filters)); | ||
| assert!(!instance_matches_filters(&json!({"count": 3, "enabled": true}), &filters)); | ||
| assert!(!instance_matches_filters(&json!({"count": 2, "enabled": false}), &filters)); | ||
| // a string pattern does not match a non-string value | ||
| let filters = to_filters(json!([{ "count": "*" }])); | ||
| assert!(!instance_matches_filters(&json!({"count": 2}), &filters)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn nested_objects_match_recursively() { | ||
| let filters = to_filters(json!([ | ||
| { "properties": { "name": "b*r" } } | ||
| ])); | ||
| assert!(instance_matches_filters(&json!({"properties": {"name": "bar", "other": 1}}), &filters)); | ||
| assert!(!instance_matches_filters(&json!({"properties": {"name": "baz"}}), &filters)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn empty_filter_object_matches_everything() { | ||
| let filters = to_filters(json!([{}])); | ||
| assert!(instance_matches_filters(&json!({"name": "anything"}), &filters)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn apply_export_filter_retains_matching() { | ||
| let mut instances = vec![ | ||
| json!({"name": "sshd", "startType": "automatic"}), | ||
| json!({"name": "spooler", "startType": "automatic"}), | ||
| json!({"name": "ssh-agent", "startType": "manual"}), | ||
| ]; | ||
| let filters = to_filters(json!([{ "name": "*ssh*" }])); | ||
| apply_export_filter(&mut instances, &filters); | ||
| assert_eq!(instances.len(), 2); | ||
| assert_eq!(instances[0]["name"], "sshd"); | ||
| assert_eq!(instances[1]["name"], "ssh-agent"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn non_object_instances_do_not_match() { | ||
| let filters = to_filters(json!([{ "name": "*" }])); | ||
| assert!(!instance_matches_filters(&json!("just a string"), &filters)); | ||
| assert!(!instance_matches_filters(&json!(42), &filters)); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.