Additionally, add explicit rules to the AQS parser for resolving shift/reduce conflicts involving NOT/OR clauses.
From: Vibhav Pant vibhavp@gmail.com
--- dlls/windows.devices.enumeration/aqs.y | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+)
diff --git a/dlls/windows.devices.enumeration/aqs.y b/dlls/windows.devices.enumeration/aqs.y index 556900ece17..53ecbd054df 100644 --- a/dlls/windows.devices.enumeration/aqs.y +++ b/dlls/windows.devices.enumeration/aqs.y @@ -90,6 +90,33 @@ expr: #endif (void)yynerrs; /* avoid unused variable warning */ } + | TK_NOT expr expr + { + struct aqs_expr *expr; + + if (FAILED(get_boolean_not_expr( ctx, $2, &expr ))) + YYABORT; + if (FAILED(join_expr( ctx, expr, $3, &$$ ))) + YYABORT; + } + | expr expr TK_OR expr + { + struct aqs_expr *expr; + + if (FAILED(join_expr( ctx, $1, $2, &expr ))) + YYABORT; + if (FAILED(get_boolean_binary_expr( ctx, DEVPROP_OPERATOR_OR_OPEN, expr, $4, &$$ ))) + YYABORT; + } + | expr TK_OR expr expr + { + struct aqs_expr *expr; + + if (FAILED(get_boolean_binary_expr( ctx, DEVPROP_OPERATOR_OR_OPEN, $1, $3, &expr ))) + YYABORT; + if (FAILED(join_expr( ctx, expr, $4, &$$ ))) + YYABORT; + } | expr TK_AND expr { if (FAILED(get_boolean_binary_expr( ctx, DEVPROP_OPERATOR_AND_OPEN, $1, $3, &$$ )))
From: Vibhav Pant vibhavp@gmail.com
--- dlls/cfgmgr32/tests/cfgmgr32.c | 115 ++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-)
diff --git a/dlls/cfgmgr32/tests/cfgmgr32.c b/dlls/cfgmgr32/tests/cfgmgr32.c index 5841a1a8d4b..6b501cf4ea3 100644 --- a/dlls/cfgmgr32/tests/cfgmgr32.c +++ b/dlls/cfgmgr32/tests/cfgmgr32.c @@ -1,5 +1,6 @@ /* * Copyright (C) 2023 Mohamad Al-Jaf + * Copyright (C) 2025 Vibhav Pant * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -859,12 +860,90 @@ static void test_DevGetObjects( void ) &bool_val, } }; + static const DEVPROP_FILTER_EXPRESSION filter_instance_id_exists = { + DEVPROP_OPERATOR_NOT_EQUALS, { { DEVPKEY_Device_InstanceId, DEVPROP_STORE_SYSTEM, NULL }, DEVPROP_TYPE_EMPTY, 0, NULL } + }; + static const DEVPROP_FILTER_EXPRESSION filter_instance_id_not_exists = { + DEVPROP_OPERATOR_EQUALS, { { DEVPKEY_Device_InstanceId, DEVPROP_STORE_SYSTEM, NULL }, DEVPROP_TYPE_EMPTY, 0, NULL } + }; + /* Test cases for filter expressions containing boolean operators. All of these filters are logically equivalent + * and should return identical objects, unless "empty" is TRUE. */ + static const struct { + const char *query; /* The query as an AQS device selector. */ + DEVPROP_FILTER_EXPRESSION expr[8]; + SIZE_T size; + BOOL empty; + } logical_op_test_cases[] = { + { + "NOT (System.Devices.InstanceId := [])", + { { DEVPROP_OPERATOR_NOT_OPEN }, filter_instance_id_not_exists, { DEVPROP_OPERATOR_NOT_CLOSE } }, + 3 + }, + { + "NOT (NOT (System.Device.InstanceId :- []))", + { { DEVPROP_OPERATOR_NOT_OPEN }, { DEVPROP_OPERATOR_NOT_OPEN }, + filter_instance_id_exists, + { DEVPROP_OPERATOR_NOT_CLOSE }, { DEVPROP_OPERATOR_NOT_CLOSE } }, + 5 + }, + { + "NOT ((System.Device.InstanceId := []) AND (System.Device.InstanceId :- []))", + { { DEVPROP_OPERATOR_NOT_OPEN }, + { DEVPROP_OPERATOR_AND_OPEN }, filter_instance_id_not_exists, filter_instance_id_exists,{ DEVPROP_OPERATOR_AND_CLOSE }, + { DEVPROP_OPERATOR_NOT_CLOSE } }, + 6 + }, + { + "NOT ((NOT (System.Device.InstanceId :- [])) AND (System.Device.InstanceId :- []))", + { { DEVPROP_OPERATOR_NOT_OPEN }, + { DEVPROP_OPERATOR_AND_OPEN }, + { DEVPROP_OPERATOR_NOT_OPEN }, filter_instance_id_exists, { DEVPROP_OPERATOR_NOT_CLOSE }, + filter_instance_id_exists, + { DEVPROP_OPERATOR_AND_CLOSE }, + { DEVPROP_OPERATOR_NOT_CLOSE } }, + 8 + }, + { + "((System.Device.InstanceId :- []) OR (System.Device.InstanceId := []))", + { { DEVPROP_OPERATOR_OR_OPEN }, filter_instance_id_exists, filter_instance_id_not_exists, { DEVPROP_OPERATOR_OR_CLOSE } }, + 4 + }, + { + "((System.Devices.InstanceId :- [] AND System.Devices.InstanceId :- []) OR System.Device.InstanceId := [])", + { { DEVPROP_OPERATOR_OR_OPEN }, + { DEVPROP_OPERATOR_AND_OPEN }, filter_instance_id_exists, filter_instance_id_exists, { DEVPROP_OPERATOR_AND_CLOSE }, + filter_instance_id_not_exists, + { DEVPROP_OPERATOR_OR_CLOSE } }, + 7 + }, + /* Filters that return empty results */ + { + "NOT (System.Devices.InstanceId :- [])", + { { DEVPROP_OPERATOR_NOT_OPEN }, filter_instance_id_exists, { DEVPROP_OPERATOR_NOT_CLOSE } }, + 3, + TRUE, + }, + { + "NOT (NOT (System.Devices.InstanceId := [] AND System.Devices.InstanceId :- []))", + { { DEVPROP_OPERATOR_NOT_OPEN }, { DEVPROP_OPERATOR_NOT_OPEN }, + { DEVPROP_OPERATOR_AND_OPEN }, filter_instance_id_not_exists, filter_instance_id_exists, { DEVPROP_OPERATOR_AND_CLOSE }, + { DEVPROP_OPERATOR_NOT_CLOSE }, { DEVPROP_OPERATOR_NOT_CLOSE } }, + 8, + TRUE, + }, + { + "(System.Devices.InstanceId := [] OR System.Devices.InstanceId := [])", + { { DEVPROP_OPERATOR_OR_OPEN }, filter_instance_id_not_exists, filter_instance_id_not_exists, { DEVPROP_OPERATOR_OR_CLOSE } }, + 4, + TRUE + } + }; DEVPROPCOMPKEY prop_iface_class = { DEVPKEY_DeviceInterface_ClassGuid, DEVPROP_STORE_SYSTEM, NULL }; DEVPROP_FILTER_EXPRESSION filters[4]; const DEV_OBJECT *objects = NULL; DEVPROPCOMPKEY prop_key = {0}; + ULONG i, len = 0, len2 = 0; HRESULT hr; - ULONG i, len = 0;
if (!pDevGetObjects || !pDevFreeObjects || !pDevFindProperty) { @@ -1158,6 +1237,40 @@ static void test_DevGetObjects( void ) pDevFreeObjects( len, objects ); bool_val = TRUE;
+ /* Get the number of objects that the filters in logical_op_test_cases should return. */ + filters[0] = filter_instance_id_exists; + len = 0; + objects = NULL; + hr = pDevGetObjects( DevObjectTypeDeviceInterface, DevQueryFlagNone, 0, NULL, 1, filters, &len, &objects ); + ok( hr == S_OK, "got hr %#lx\n", hr ); + ok( len > 0, "got len %lu\n", len ); + ok( !!objects, "got objects %p\n", objects ); + pDevFreeObjects( len, objects ); + + for (i = 0; i < ARRAY_SIZE( logical_op_test_cases ); i++ ) + { + winetest_push_context( "i=%lu (%s)", i, debugstr_a( logical_op_test_cases[i].query ) ); + + len2 = 0; + objects = NULL; + hr = pDevGetObjects( DevObjectTypeDeviceInterface, DevQueryFlagNone, 0, NULL, logical_op_test_cases[i].size, + logical_op_test_cases[i].expr, &len2, &objects ); + ok( hr == S_OK, "got hr %#lx\n", hr ); + if (logical_op_test_cases[i].empty) + { + todo_wine_if( len2 ) ok( !len2, "got len2 %lu\n", len2 ); + todo_wine_if( len2 ) ok( !objects, "got objects %p\n", objects ); + } + else + { + todo_wine_if( !len2 ) ok( len2 == len, "got len2 %lu != %lu\n", len2, len ); + todo_wine_if( !len2 ) ok( !!objects, "got objects %p\n", objects ); + } + pDevFreeObjects( len2, objects ); + + winetest_pop_context(); + } + for (i = 0; i < ARRAY_SIZE( test_cases ); i++) { const DEV_OBJECT *objects = NULL;
From: Vibhav Pant vibhavp@gmail.com
--- dlls/cfgmgr32/main.c | 97 +++++++++++++++---- dlls/cfgmgr32/tests/cfgmgr32.c | 8 +- .../tests/devices.c | 4 +- 3 files changed, 84 insertions(+), 25 deletions(-)
diff --git a/dlls/cfgmgr32/main.c b/dlls/cfgmgr32/main.c index 69004ea4b5c..5f2eabfc84f 100644 --- a/dlls/cfgmgr32/main.c +++ b/dlls/cfgmgr32/main.c @@ -17,6 +17,8 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */
+ #include <assert.h> + #include "wine/debug.h" #include "wine/rbtree.h" #include "winreg.h" @@ -414,6 +416,9 @@ static HRESULT devprop_filter_eval_compare( const DEV_OBJECT *obj, const DEVPROP cmp = op & DEVPROP_OPERATOR_MODIFIER_IGNORE_CASE ? wcsicmp( prop->Buffer, cmp_prop->Buffer ) : wcscmp( prop->Buffer, cmp_prop->Buffer ); break; + case DEVPROP_TYPE_GUID: + /* Any other comparison operator other than DEVPROP_OPERATOR_EQUALS with GUIDs evaluates to false. */ + if (!(op & DEVPROP_OPERATOR_EQUALS)) break; default: cmp = memcmp( prop->Buffer, cmp_prop->Buffer, prop->BufferSize ); break; @@ -440,47 +445,101 @@ static HRESULT devprop_filter_eval_compare( const DEV_OBJECT *obj, const DEVPROP return ret ? S_OK : S_FALSE; }
-/* Return S_OK if the specified filter expressions match the object, S_FALSE if it doesn't. */ +/* Return S_OK if the specified filter expressions match the object, S_FALSE if it doesn't. + * op_outer_logical is the logical operator denoting the outer expression this sub-expression is inside. + * op_outer_logical can be DEVPROP_OPERATOR_NONE (in which case filters_len is used), or one of the logical _CLOSE + * operators (and filters_len gets ignored, and evaluation is stopped at the corresponding CLOSE operator). + * When op_outer_logical is not DEVPROP_OPERATOR_NONE, last_expr_idx gets set to the index where this sub-expression ends. */ static HRESULT devprop_filter_matches_object( const DEV_OBJECT *obj, ULONG filters_len, - const DEVPROP_FILTER_EXPRESSION *filters ) + const DEVPROP_OPERATOR op_outer_logical, + const DEVPROP_FILTER_EXPRESSION *filters, ULONG *last_expr_idx ) { + const DEVPROP_OPERATOR op_outer_close = op_outer_logical + (DEVPROP_OPERATOR_AND_CLOSE - DEVPROP_OPERATOR_AND_OPEN); HRESULT hr = S_OK; ULONG i;
- TRACE( "(%s, %lu, %p)\n", debugstr_DEV_OBJECT( obj ), filters_len, filters ); + TRACE( "(%s, %lu, %s, %p)\n", debugstr_DEV_OBJECT( obj ), filters_len, + debugstr_DEVPROP_OPERATOR( op_outer_logical ), filters );
- if (!filters_len) + if (op_outer_logical) + assert( op_outer_logical == DEVPROP_OPERATOR_AND_OPEN || op_outer_logical == DEVPROP_OPERATOR_OR_OPEN || + op_outer_logical == DEVPROP_OPERATOR_NOT_OPEN ); + else if (!filters_len) return S_OK;
- /* By default, the evaluation is performed by AND-ing all individual filter expressions. */ - for (i = 0; i < filters_len; i++) + for (i = 0; op_outer_logical ? filters[i].Operator != op_outer_close : i < filters_len; i++) { const DEVPROP_FILTER_EXPRESSION *filter = &filters[i]; - DEVPROP_OPERATOR op = filter->Operator; + DEVPROP_OPERATOR op = filter->Operator, logical;
- if (op == DEVPROP_OPERATOR_NONE) - { - hr = S_FALSE; - break; - } if (op & (DEVPROP_OPERATOR_MASK_LIST | DEVPROP_OPERATOR_MASK_ARRAY)) { FIXME( "Unsupported list/array operator: %s\n", debugstr_DEVPROP_OPERATOR( op ) ); continue; } - if (op & DEVPROP_OPERATOR_MASK_LOGICAL) + + logical = op & DEVPROP_OPERATOR_MASK_LOGICAL; + switch (logical) { - FIXME( "Unsupported logical operator: %s\n", debugstr_DEVPROP_OPERATOR( op ) ); - continue; + case DEVPROP_OPERATOR_NONE: + if (op == DEVPROP_OPERATOR_NONE) + hr = S_FALSE; + else if ((op & DEVPROP_OPERATOR_MASK_EVAL) && FAILED(hr = devprop_filter_eval_compare( obj, filter ))) + goto done; + break; + case DEVPROP_OPERATOR_AND_OPEN: + case DEVPROP_OPERATOR_OR_OPEN: + case DEVPROP_OPERATOR_NOT_OPEN: + { + DEVPROP_OPERATOR op_close = logical + (DEVPROP_OPERATOR_AND_CLOSE - DEVPROP_OPERATOR_AND_OPEN); + ULONG last_idx; + + /* Filter expression is validated, so we know there is atleast 1 non-logical expression + DEVPROP_OPERATOR_CLOSE_* after i. */ + hr = devprop_filter_matches_object( obj, 0, logical, &filters[i + 1], &last_idx ); + i += last_idx + 1; + if (FAILED( hr )) goto done; + assert( filters[i].Operator == op_close ); + break; + } + DEFAULT_UNREACHABLE; } - if (op & DEVPROP_OPERATOR_MASK_EVAL) + + /* See if we can short-circuit. */ + switch (op_outer_logical) { - hr = devprop_filter_eval_compare( obj, filter ); - if (FAILED( hr ) || hr == S_FALSE) + /* {NOT_OPEN, ..., NOT_CLOSE} is the same as {NOT_OPEN, AND_OPEN, ..., AND_CLOSE, NOT_CLOSE}, so we can + * short circuit here as well. */ + case DEVPROP_OPERATOR_NOT_OPEN: + case DEVPROP_OPERATOR_AND_OPEN: + if (hr == S_FALSE) + { + /* Skip to the end of this sub-expression. */ + while (filters[++i].Operator != op_outer_close) /* nothing */; + goto done; + } + break; + case DEVPROP_OPERATOR_NONE: + /* Same as AND. */ + if (hr == S_FALSE) goto done; + break; + case DEVPROP_OPERATOR_OR_OPEN: + if (hr == S_OK) + { + while (filters[++i].Operator != DEVPROP_OPERATOR_OR_CLOSE) /* nothing */; + goto done; + } break; + DEFAULT_UNREACHABLE; } }
+done: + if (SUCCEEDED( hr ) && op_outer_logical) + { + if (last_expr_idx) *last_expr_idx = i; + if (op_outer_logical == DEVPROP_OPERATOR_NOT_OPEN) + hr = (hr == S_OK) ? S_FALSE : S_OK; + } return hr; }
@@ -859,7 +918,7 @@ static HRESULT enum_dev_objects( DEV_OBJECT_TYPE type, ULONG props_len, const DE { if (filters_len) { - hr = devprop_filter_matches_object( &obj, filters_len, filters ); + hr = devprop_filter_matches_object( &obj, filters_len, DEVPROP_OPERATOR_NONE, filters, NULL ); /* Shrink pProperties to only the desired ones, unless DevQueryFlagAllProperties is set. */ if (!all_props) dev_object_remove_unwanted_props( &obj, props_len, props ); diff --git a/dlls/cfgmgr32/tests/cfgmgr32.c b/dlls/cfgmgr32/tests/cfgmgr32.c index 6b501cf4ea3..c902fecc006 100644 --- a/dlls/cfgmgr32/tests/cfgmgr32.c +++ b/dlls/cfgmgr32/tests/cfgmgr32.c @@ -1258,13 +1258,13 @@ static void test_DevGetObjects( void ) ok( hr == S_OK, "got hr %#lx\n", hr ); if (logical_op_test_cases[i].empty) { - todo_wine_if( len2 ) ok( !len2, "got len2 %lu\n", len2 ); - todo_wine_if( len2 ) ok( !objects, "got objects %p\n", objects ); + ok( !len2, "got len2 %lu\n", len2 ); + ok( !objects, "got objects %p\n", objects ); } else { - todo_wine_if( !len2 ) ok( len2 == len, "got len2 %lu != %lu\n", len2, len ); - todo_wine_if( !len2 ) ok( !!objects, "got objects %p\n", objects ); + ok( len2 == len, "got len2 %lu != %lu\n", len2, len ); + ok( !!objects, "got objects %p\n", objects ); } pDevFreeObjects( len2, objects );
diff --git a/dlls/windows.devices.enumeration/tests/devices.c b/dlls/windows.devices.enumeration/tests/devices.c index 433f9c33f2b..5c7e1fdc0f2 100644 --- a/dlls/windows.devices.enumeration/tests/devices.c +++ b/dlls/windows.devices.enumeration/tests/devices.c @@ -818,7 +818,7 @@ static void test_aqs_filters( void ) test_FindAllAsyncAqsFilter( statics, filters_empty, FALSE, FALSE ); test_CreateWatcherAqsFilter( statics, filters_empty, FALSE, FALSE, FALSE, FALSE );
- test_FindAllAsyncAqsFilter( statics, filters_boolean_op, FALSE, TRUE ); + test_FindAllAsyncAqsFilter( statics, filters_boolean_op, FALSE, FALSE ); test_CreateWatcherAqsFilter( statics, filters_boolean_op, FALSE, FALSE, FALSE, TRUE );
test_FindAllAsyncAqsFilter( statics, filters_simple, FALSE, FALSE ); @@ -827,7 +827,7 @@ static void test_aqs_filters( void ) test_FindAllAsyncAqsFilter( statics, filters_case_insensitive, TRUE, FALSE ); test_CreateWatcherAqsFilter( statics, filters_case_insensitive, TRUE, FALSE, FALSE, FALSE );
- test_FindAllAsyncAqsFilter( statics, filters_precedence, FALSE, TRUE ); + test_FindAllAsyncAqsFilter( statics, filters_precedence, FALSE, FALSE ); test_CreateWatcherAqsFilter( statics, filters_precedence, FALSE, FALSE, FALSE, TRUE );
test_FindAllAsyncAqsFilter( statics, filters_no_results, FALSE, FALSE );
Rémi Bernon (@rbernon) commented about dlls/cfgmgr32/main.c:
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
- #include <assert.h>
```suggestion:-0+0 #include <assert.h> ```
Rémi Bernon (@rbernon) commented about dlls/cfgmgr32/main.c:
cmp = op & DEVPROP_OPERATOR_MODIFIER_IGNORE_CASE ? wcsicmp( prop->Buffer, cmp_prop->Buffer ) : wcscmp( prop->Buffer, cmp_prop->Buffer ); break;
case DEVPROP_TYPE_GUID:
/* Any other comparison operator other than DEVPROP_OPERATOR_EQUALS with GUIDs evaluates to false. */
if (!(op & DEVPROP_OPERATOR_EQUALS)) break;
Wouldn't that get inverted by the `DEVPROP_OPERATOR_MODIFIER_NOT` check at the bottom? Is it expected?
Rémi Bernon (@rbernon) commented about dlls/cfgmgr32/main.c:
DEFAULT_UNREACHABLE;
}
/* See if we can short-circuit. */
switch (op_outer_logical)
{
/* {NOT_OPEN, ..., NOT_CLOSE} is the same as {NOT_OPEN, AND_OPEN, ..., AND_CLOSE, NOT_CLOSE}, so we can
* short circuit here as well. */
case DEVPROP_OPERATOR_NOT_OPEN:
case DEVPROP_OPERATOR_AND_OPEN:
if (hr == S_FALSE)
{
/* Skip to the end of this sub-expression. */
while (filters[++i].Operator != op_outer_close) /* nothing */;
goto done; }
I'm not 100% sure but this looks suspicious, what if there's multiple nested blocks, like `{AND_OPEN, NONE, {AND_OPEN, NONE, AND_CLOSE}, NONE, AND_CLOSE}`? The function looks complicated, it's hard to tell and there's probably a cleaner way to split the logic.