From b28fb85a1f88aa55db07a53eae32bd663a3e08d8 Mon Sep 17 00:00:00 2001 From: Noel Power Date: Wed, 29 Jun 2016 11:29:54 +0100 Subject: [PATCH] libcli/wsp: Add support for simplified Advanced Query Syntax Add support to parse AQS-like (Advanced query syntax) AQS - see https://learn.microsoft.com/en-gb/windows/win32/search/-search-3x-advancedquerysyntax The basic (AQS) syntax is supported e.g. a query is built of a sequence of queries connected by AND, OR and NOT where the query elements are essentially restrictions defined by a property. There are some limitations on the operators supported[1] and additionally some things like enumerated ranges are not supported at all and range values are not delimited as specified [2]. Some special cases that you see in the windows search UI are exceptions [3] which are handled more or less as keywords Some examples: The following are all exactly the same query just expressed using different variations of the syntax 'ALL:($ System.Size:10241-102401' 'ALL:$ System.Size:>=10241 AND System.Size:<102401' 'ALL:$ System.Size:small' The queries above by default select the property System.ItemUrl as the one and only column returned, the query parameter however accepts a variation to the AQS like syntax to allow arbitrary columns to be selected e.g. 'SELECT System.ItemName, System.ItemURL, System.Size WHERE ALL:$ Greater than < Less than >= Greater than or equals <= Less than or equals $= equals $< starts with [2] ranges are specified as value-value instead of value..value (seems my flex/bison skills are not good enough and couldn't get that to work with '..' [3] The windows UI has shortcut ranges (presumably represented as enumerated ranges) providing date ranges like 'today', 'tomorrow', 'lastweek' etc. and similarly sizes like "empty, tiny, small, large..." These are supported (but implemented as keywords) Signed-off-by: Noel Power Reviewed-by: Andrew Bartlett --- libcli/wsp/wscript_build | 33 ++ libcli/wsp/wsp_aqs.c | 877 ++++++++++++++++++++++++++++++++++++ libcli/wsp/wsp_aqs.h | 166 +++++++ libcli/wsp/wsp_aqs_lexer.l | 152 +++++++ libcli/wsp/wsp_aqs_parser.y | 422 +++++++++++++++++ wscript_build | 1 + 6 files changed, 1651 insertions(+) create mode 100644 libcli/wsp/wscript_build create mode 100644 libcli/wsp/wsp_aqs.c create mode 100644 libcli/wsp/wsp_aqs.h create mode 100644 libcli/wsp/wsp_aqs_lexer.l create mode 100644 libcli/wsp/wsp_aqs_parser.y diff --git a/libcli/wsp/wscript_build b/libcli/wsp/wscript_build new file mode 100644 index 00000000000..7aadbad8f14 --- /dev/null +++ b/libcli/wsp/wscript_build @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +#default flex recepie doesn't create a header file +bld.SAMBA_GENERATOR('wsp_flex', + source='wsp_aqs_lexer.l', + target='wsp_aqs_lexer.h wsp_aqs_lexer.c', + group='build_source', + rule='${FLEX} --header-file=${TGT[0].abspath(env)} --outfile=${TGT[1].abspath(env)} ${SRC[0].abspath(env)}', + enabled=bld.env.with_wsp + ) + +# With centos7-o3 CI job (and gcc 4.8.5) we get +# an error with -Wstrict-overflow. +# Same code is good with gcc version +# gcc 8.5.0 (centos8) and whatever versions of +# gcc we have in the other XXXX-o3 images. +# We turn off strict-overflow just for this generated +# file +parser_cflags='' +if bld.CONFIG_SET('HAVE_WNO_STRICT_OVERFLOW'): + parser_cflags += ' -Wno-strict-overflow' + +bld.SAMBA_SUBSYSTEM('LIBSAMBA_WSP_PARSER', + source='wsp_aqs_parser.y', + deps='talloc wsp_flex', + cflags_end=parser_cflags, + enabled=bld.env.with_wsp + ) +bld.SAMBA_SUBSYSTEM('LIBSAMBA_WSP', + source='wsp_aqs.c wsp_aqs_lexer.c', + public_deps='LIBSAMBA_WSP_PARSER', + enabled=bld.env.with_wsp + ) diff --git a/libcli/wsp/wsp_aqs.c b/libcli/wsp/wsp_aqs.c new file mode 100644 index 00000000000..acf12293daf --- /dev/null +++ b/libcli/wsp/wsp_aqs.c @@ -0,0 +1,877 @@ +/* + * Window Search Service + * + * Copyright (c) Noel Power + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include "includes.h" +#include "libcli/wsp/wsp_aqs.h" +#include "libcli/wsp/wsp_aqs_parser.tab.h" +#include "libcli/wsp/wsp_aqs_lexer.h" +#include "librpc/wsp/wsp_util.h" +#include "librpc/gen_ndr/ndr_wsp.h" +#include +#include + +int yyparse(t_select_stmt **select, yyscan_t scanner); + +static void reverse_cols(t_select_stmt *select) +{ + int num_elems, fwd, rev; + char **cols; + + + if (!select->cols) { + return; + } + num_elems = select->cols->num_cols; + cols = select->cols->cols; + + for(fwd = 0, rev = num_elems - 1; fwd <= rev; fwd++, rev--) { + char * tmp = cols[rev]; + cols[rev] = cols[fwd]; + cols[fwd] = tmp; + } + +} + +t_select_stmt *get_wsp_sql_tree(const char *expr) +{ + t_select_stmt *select = NULL; + yyscan_t scanner; + YY_BUFFER_STATE state; + + if (yylex_init(&scanner)) { + DBG_ERR("couldn't initialize\n"); + return NULL; + } + + state = yy_scan_string(expr, scanner); + + if (yyparse(&select, scanner)) { + DBG_ERR("some parse error\n"); + return NULL; + } + /* + * parsed columns are in reverse order to how they are specified + * in the AQS like statement, reverse them again to correct this. + */ + reverse_cols(select); + + yy_delete_buffer(state, scanner); + + yylex_destroy(scanner); + + return select; +} + + +t_col_list *create_cols(TALLOC_CTX *ctx, const char *col, t_col_list *append_list) +{ + t_col_list *cols = append_list; + if (!get_prop_info(col)) { + DBG_ERR("Unknown property %s\n", col); + return NULL; + } + if (cols == NULL) { + cols = talloc_zero(ctx, t_col_list); + if (cols == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + cols->num_cols = 0; + cols->cols = NULL; + DBG_INFO("returning new cols %p with item %s\n", cols, col); + } + if (col) { + int old_index = cols->num_cols; + if (old_index == 0) { + cols->cols = talloc_array(cols, char*, 1); + } else { + cols->cols = (char **)talloc_realloc(cols, cols->cols, char*, old_index + 1); + } + if (!cols->cols) { + return NULL; /* can we create a parser error here */ + } + cols->num_cols++; + cols->cols[old_index] = talloc_strdup(cols, col); + if (cols->cols[old_index] == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + + } + return cols; +} + +t_select_stmt *create_select(TALLOC_CTX *ctx, t_col_list *cols, t_query *where) +{ + t_select_stmt *result = talloc_zero(ctx, t_select_stmt); + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + result->cols = cols; + result->where = where; + return result; +} + +t_basic_restr *create_basic_restr(TALLOC_CTX *ctx, + uint32_t prop_type, + t_optype op, + t_value_holder *values) +{ + t_basic_restr *result = talloc_zero(ctx, t_basic_restr); + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + result->prop_type = prop_type; + result->op = op; + if (values->type == VALUE_RANGE) { + t_restr *left_node; + t_restr *right_node; + t_basic_restr *left_val; + t_basic_restr *right_val; + if (op != eEQ) { + DBG_ERR("Unsupported operation %d\n", op); + TALLOC_FREE(result); + goto out; + } + + if (values->value.value_range->lower == NULL) { + DBG_ERR("range lower limit doesn't exist\n"); + TALLOC_FREE(result); + goto out; + } + /* + * detect special case where upper range doesn't exist + * and convert to a property value. (this won't happen from + * the cmdline directly but only as a result of a range + * created 'specially' in code, e.g. special gigantic size + * range. + */ + if (values->value.value_range->upper == NULL) { + result->op = eGE; + result->values = values->value.value_range->lower; + goto out; + } + result->values = talloc_zero(result, t_value_holder); + if (result->values == NULL) { + DBG_ERR("out of memory\n"); + TALLOC_FREE(result); + goto out; + } + /* + * try create a restriction tree (>=lower AND values, + prop_type, + eGE, + values->value.value_range->lower); + + right_val = create_basic_restr(result->values, + prop_type, + eLT, + values->value.value_range->upper); + + if (!left_val || !right_val) { + DBG_ERR("Failed creating basic_restriction values " + "for range\n"); + TALLOC_FREE(result); + goto out; + } + + left_node = create_restr(result->values, eVALUE, NULL, NULL, left_val); + right_node = create_restr(result->values, eVALUE, NULL, NULL, right_val); + + + if (!left_node || !right_node) { + DBG_ERR("Failed creating restr nodes for range\n"); + TALLOC_FREE(result); + goto out; + } + result->values->type = RESTR; + result->values->value.restr_tree = create_restr(result->values, + eAND, + left_node, + right_node, + NULL); + if (!result->values->value.restr_tree) { + DBG_ERR("Failed creating restr tree for range\n"); + TALLOC_FREE(result); + goto out; + } + } else { + result->values = values; + } +out: + return result; +} + +/* + * The parser reads numbers as VT_UI8, booleans as VT_BOOL and strings as + * VT_LPWSTR + */ +typedef bool (*conv_func) (TALLOC_CTX *ctx, t_value_holder *src, + struct wsp_cbasestoragevariant *dest, + uint16_t dest_type); + +/* + * default converter #TODO probably should cater for detecting over/underrun + * depending on the dest_type we are narrowing to + */ +static bool default_convertor(TALLOC_CTX *ctx, + t_value_holder *src, + struct wsp_cbasestoragevariant *dest, + uint16_t dest_type) +{ + if (src->type != NUMBER) { + return false; + } + dest->vvalue.vt_ui8 = src->value.number; + dest->vtype = dest_type; + return true; +} + +static bool convert_string_to_lpwstr_v(TALLOC_CTX *ctx, + t_value_holder *src, + struct wsp_cbasestoragevariant *dest, + uint16_t dest_type) +{ + const char *str = src->value.string; + set_variant_lpwstr_vector(ctx, dest, &str, 1); + return true; +} + +static bool convert_string_to_lpwstr(TALLOC_CTX *ctx, + t_value_holder *src, + struct wsp_cbasestoragevariant *dest, + uint16_t dest_type) +{ + const char *str = src->value.string; + set_variant_lpwstr(ctx, dest, str); + return true; +} + +static bool convert_bool_to_lpwstr(TALLOC_CTX *ctx, + t_value_holder *src, + struct wsp_cbasestoragevariant *dest, + uint16_t dest_type) +{ + set_variant_lpwstr( + ctx, + dest, + src->value.boolean ? "true": "false"); + return true; +} + +static bool convert_string_to_filetime(TALLOC_CTX *ctx, + t_value_holder *src, + struct wsp_cbasestoragevariant *dest, + uint16_t dest_type) +{ + + static const char *fmts[] = { + "%FT%TZ", + "%FT%T", + "%F %T", + "%F %R", + "%F", + }; + struct tm tm; + time_t timeval = 0; + int i; + ZERO_STRUCT(tm); + + for (i = 0; i < ARRAY_SIZE(fmts); i++) { + if (strptime(src->value.string, fmts[i], &tm)) { + timeval = timegm(&tm); + break; + } + } + + if (timeval) { + NTTIME nt; + unix_to_nt_time(&nt, timeval); + dest->vtype = VT_FILETIME; + dest->vvalue.vt_filetime = nt; + return true; + } + return false; +} + +const struct { + uint16_t src_vtype; + uint16_t dest_vtype; + conv_func convert_type; +} type_conv_map[] = { + {NUMBER, VT_I8, default_convertor}, + {NUMBER, VT_UI8, default_convertor}, + {NUMBER, VT_INT, default_convertor}, + {NUMBER, VT_UINT, default_convertor}, + {NUMBER, VT_I4, default_convertor}, + {NUMBER, VT_UI4, default_convertor}, + {NUMBER, VT_I2, default_convertor}, + {NUMBER, VT_UI2, default_convertor}, + {NUMBER, VT_BOOL, default_convertor}, + {NUMBER, VT_FILETIME, default_convertor}, + {NUMBER, VT_BOOL, default_convertor}, + {BOOL, VT_LPWSTR, convert_bool_to_lpwstr}, + {STRING, VT_LPWSTR, convert_string_to_lpwstr}, + {STRING, VT_LPWSTR | VT_VECTOR, convert_string_to_lpwstr_v}, + {STRING, VT_FILETIME, convert_string_to_filetime}, +}; + +static bool process_prop_value(TALLOC_CTX *ctx, + const struct full_propset_info *prop_info, + t_value_holder *node_value, + struct wsp_cbasestoragevariant *prop_value) +{ + int i; + + /* coerce type as required */ + for (i = 0; i < ARRAY_SIZE(type_conv_map); i++ ) { + if (type_conv_map[i].src_vtype == node_value->type && + type_conv_map[i].dest_vtype == prop_info->vtype) { + type_conv_map[i].convert_type(ctx, + node_value, + prop_value, + prop_info->vtype); + return true; + } + } + return false; +} + +t_basic_query *create_basic_query(TALLOC_CTX *ctx, const char *propname, t_basic_restr *restr) +{ + t_basic_query *result = talloc_zero(ctx, t_basic_query); + if (result == NULL) { + DBG_ERR("out of memory\n"); + goto out; + } + result->prop = talloc_strdup(result, propname); + result->prop_info = get_propset_info_with_guid(propname, &result->guid); + + if (!result->prop_info) { + DBG_ERR("Unknown property %s\n",propname); + TALLOC_FREE(result); + goto out; + } + result->basic_restriction = restr; +out: + return result; +} + +static struct wsp_crestriction *create_restriction(TALLOC_CTX *ctx, + t_basic_query *query) +{ + struct wsp_crestriction *crestriction = NULL; + struct wsp_cfullpropspec *prop = NULL; + t_basic_restr *restr = NULL; + t_value_holder *src = NULL; + crestriction = talloc_zero(ctx, struct wsp_crestriction); + if (crestriction == NULL) { + DBG_ERR("out of memory\n"); + goto done; + } + + restr = query->basic_restriction; + src = restr->values; + + if (restr->prop_type == RTNONE) { + /* shouldn't end up here */ + DBG_ERR("Unexpected t_basic_restr type\n"); + TALLOC_FREE(crestriction); + goto done; + } + + crestriction->weight = 1000; + + if (restr->prop_type == RTCONTENT) { + struct wsp_ccontentrestriction *content = NULL; + crestriction->ultype = RTCONTENT; + if (src->type != STRING) { + DBG_ERR("expected string value for %s\n", + query->prop); + TALLOC_FREE(crestriction); + goto done; + } + content = &crestriction->restriction.ccontentrestriction; + content->pwcsphrase = src->value.string; + content->cc = strlen(src->value.string); + /* + * In the future we might generate the lcid from + * environ (or config) + */ + content->lcid = WSP_DEFAULT_LCID; + if (restr->op == eEQUALS) { + content->ulgeneratemethod = 0; + } else { + content->ulgeneratemethod = 1; + } + + prop = &content->property; + } else if (restr->prop_type == RTPROPERTY) { + struct wsp_cbasestoragevariant *dest = + &crestriction->restriction.cpropertyrestriction.prval; + crestriction->ultype = RTPROPERTY; + if (!process_prop_value(ctx, query->prop_info, src, dest)) { + DBG_ERR("Failed to process value for property %s\n", + query->prop); + TALLOC_FREE(crestriction); + goto done; + } + crestriction->restriction.cpropertyrestriction.relop = + restr->op; + prop = &crestriction->restriction.cpropertyrestriction.property; + } else { + TALLOC_FREE(crestriction); + goto done; + } + prop->guidpropset = query->guid; + prop->ulkind = PRSPEC_PROPID; + prop->name_or_id.prspec = query->prop_info->id; +done: + return crestriction; +} + +/* expands restr_node into a tree of t_query nodes */ +static void build_query(TALLOC_CTX *ctx, t_query *node, t_restr *restr_node, + const char* prop) +{ + if (!node) { + return; + } + if (!restr_node) { + return; + } + + node->type = restr_node->type; + + if (restr_node->left) { + node->left = talloc_zero(ctx, t_query); + SMB_ASSERT(node->left != NULL); + build_query(ctx, node->left, restr_node->left, prop); + } + + if (restr_node->right) { + node->right = talloc_zero(ctx, t_query); + SMB_ASSERT(node->right != NULL); + build_query(ctx, node->right, restr_node->right, prop); + } + + if (restr_node->type == eVALUE) { + node->restriction = + create_restriction(ctx, + create_basic_query(ctx, + prop, + restr_node->basic_restr)); + } +} + +t_query *create_query_node(TALLOC_CTX *ctx, t_nodetype op, t_query *left, t_query *right, t_basic_query *value) +{ + t_query *result = talloc_zero(ctx, t_query); + if (result == NULL) { + return result; + } + result->type = op; + result->left = left; + result->right = right; + if (op == eVALUE) { + t_basic_restr *restr = value->basic_restriction; + /* expand restr node */ + if (restr->values->type == RESTR) { + build_query(ctx, + result, + restr->values->value.restr_tree, + value->prop); + } else { + result->restriction = + create_restriction(ctx, value); + if (!result->restriction) { + TALLOC_FREE(result); + } + } + } + return result; +} + +t_restr *create_restr(TALLOC_CTX *ctx, t_nodetype op, t_restr *left, t_restr *right, t_basic_restr *value) +{ + t_restr *result = talloc_zero(ctx, t_restr); + if (result == NULL) { + return result; + } + result->type = op; + result->right = right; + result->left = left; + result->basic_restr = value; + return result; +} + +t_value_holder *create_string_val(TALLOC_CTX* ctx, const char *text) +{ + t_value_holder *result = + talloc_zero(ctx, t_value_holder); + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + result->value.string = text; + result->type = STRING; + return result; +} + +t_value_holder *create_num_val(TALLOC_CTX* ctx, int64_t val) +{ + t_value_holder *result = + talloc_zero(ctx, t_value_holder); + + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + + result->type = NUMBER; + result->value.number = val; + return result; +} + +t_value_holder *create_bool_val(TALLOC_CTX* ctx, bool val) +{ + t_value_holder *result = + talloc_zero(ctx, t_value_holder); + + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + + result->type = BOOL; + result->value.boolean = val; + return result; +} + +t_value_holder *create_value_range(TALLOC_CTX* ctx, + t_value_holder *left, + t_value_holder *right) +{ + t_value_holder *result = + talloc_zero(ctx, t_value_holder); + + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + + result->type = VALUE_RANGE; + result->value.value_range = talloc_zero(result, struct value_range); + if (!result->value.value_range) { + TALLOC_FREE(result); + goto out; + } + result->value.value_range->lower = left; + result->value.value_range->upper = right; +out: + return result; +} + +static void zero_time(struct tm *tm) +{ + tm->tm_hour = 0; + tm->tm_min = 0; + tm->tm_sec = 0; +} + +typedef bool (*daterange_func) (TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2); + + +static bool create_date_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2, + int32_t lower_mday_adj, + int32_t lower_mon_adj, + int32_t upper_mday_adj, + int32_t upper_mon_adj) +{ + struct tm tm_now; + time_t now; + + struct tm tm_tmp; + time_t lower; + time_t upper; + + time(&now); + gmtime_r(&now, &tm_now); + + tm_tmp = tm_now; + zero_time(&tm_tmp); + tm_tmp.tm_mday += lower_mday_adj; + tm_tmp.tm_mon += lower_mon_adj; + lower = mktime(&tm_tmp); + tm_tmp = tm_now; + zero_time(&tm_tmp); + tm_tmp.tm_mday += upper_mday_adj; + tm_tmp.tm_mon += upper_mon_adj; + upper = mktime(&tm_tmp); + unix_to_nt_time(date1, lower); + unix_to_nt_time(date2, upper); + return true; +} + +static void get_now_tm(struct tm *tm_now) +{ + time_t now; + time(&now); + gmtime_r(&now, tm_now); +} + +static bool create_thismonth_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + struct tm tm_now; + int32_t firstofmonth_adj; + + get_now_tm(&tm_now); + firstofmonth_adj = 1 - tm_now.tm_mday; + return create_date_range(ctx, date1, + date2, firstofmonth_adj, + 0, firstofmonth_adj, 1); +} + +static bool create_lastyear_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + struct tm tm_now; + int32_t firstofmonth_adj; + int32_t january_adj; + get_now_tm(&tm_now); + + firstofmonth_adj = 1 - tm_now.tm_mday; + january_adj = -tm_now.tm_mon; + return create_date_range(ctx, date1, + date2, firstofmonth_adj, + january_adj - 12, firstofmonth_adj, january_adj); +} + +static bool create_thisyear_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + struct tm tm_now; + int32_t firstofmonth_adj; + int32_t january_adj; + + get_now_tm(&tm_now); + + firstofmonth_adj = 1 - tm_now.tm_mday; + january_adj = -tm_now.tm_mon; + return create_date_range(ctx, date1, + date2, firstofmonth_adj, + january_adj, firstofmonth_adj, january_adj + 12); +} + +static bool create_lastmonth_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + struct tm tm_now; + int32_t firstofmonth_adj; + get_now_tm(&tm_now); + + firstofmonth_adj = 1 - tm_now.tm_mday; + return create_date_range(ctx, date1, + date2, firstofmonth_adj, + -1, firstofmonth_adj, 0); +} + +static bool create_today_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + return create_date_range(ctx, date1, + date2, 0, 0, 1, 0); +} + +static bool create_yesterday_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + return create_date_range(ctx, date1, + date2, -1, 0, 0, 0); +} + +static bool create_thisweek_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + struct tm tm_now; + time_t now; + int32_t startofweek_adj; + time(&now); + gmtime_r(&now, &tm_now); + if (tm_now.tm_wday) { + startofweek_adj = 1 - tm_now.tm_wday; + } else { + startofweek_adj = -6; + } + /* lower will be the start of this week */ + return create_date_range(ctx, date1, + date2, startofweek_adj, + 0, startofweek_adj + 7, 0); +} + +static bool create_lastweek_range(TALLOC_CTX *ctx, uint64_t *date1, + uint64_t *date2) +{ + struct tm tm_now; + time_t now; + int32_t startofweek_adj; + time(&now); + gmtime_r(&now, &tm_now); + if (tm_now.tm_wday) { + startofweek_adj = 1 - tm_now.tm_wday; + } else { + startofweek_adj = -6; + } + /* upper will be the start of this week */ + return create_date_range(ctx, date1, + date2, startofweek_adj - 7, + 0,startofweek_adj, 0); +} + +t_value_holder *create_date_range_shortcut(TALLOC_CTX *ctx, + daterange_type daterange) +{ + int i; + static const struct { + daterange_type range; + daterange_func create_fn; + } date_conv_map[] = { + {eYESTERDAY, create_yesterday_range}, + {eTODAY, create_today_range}, + {eTHISMONTH, create_thismonth_range}, + {eLASTMONTH, create_lastmonth_range}, + {eTHISWEEK, create_thisweek_range}, + {eLASTWEEK, create_lastweek_range}, + {eTHISYEAR, create_thisyear_range}, + {eLASTYEAR, create_lastyear_range}, + }; + t_value_holder *result = NULL; + t_value_holder *lower = NULL; + t_value_holder *upper = NULL; + + lower = talloc_zero(ctx, t_value_holder); + if (lower == NULL) { + DBG_ERR("out of memory\n"); + goto out; + } + + upper = talloc_zero(ctx, t_value_holder); + if (upper == NULL) { + DBG_ERR("out of memory\n"); + goto out; + } + + result = create_value_range(result, lower, upper); + + if (result == NULL) { + TALLOC_FREE(result); + goto out; + } + + lower->type = NUMBER; + upper->type = NUMBER; + + result->value.value_range->lower = lower; + result->value.value_range->upper = upper; + + for (i = 0; i < ARRAY_SIZE(date_conv_map); i++) { + if (date_conv_map[i].range == daterange) { + if (!date_conv_map[i].create_fn(result, + &lower->value.number, + &upper->value.number)) { + TALLOC_FREE(result); + break; + } + break; + } + } +out: + return result; +} + +t_value_holder *create_size_range_shortcut(TALLOC_CTX *ctx, + sizerange_type sizerange) +{ + static const struct { + sizerange_type range; + uint32_t lower; + uint32_t upper; + } sizes[] = { + {eEMPTY, 0x0, 0x1}, + {eTINY, 0x1, 0x2801}, + {eSMALL, 0x2801, 0x19001}, + {eMEDIUM, 0x19001, 0x100001}, + {eLARGE, 0x100001, 0x10000001}, + {eHUGE, 0x10000001, 0x80000001}, + {eGIGANTIC, 0x80000001, 0} /* special case not a range */ + }; + int i; + t_value_holder *result = NULL; + uint32_t lower_size; + uint32_t upper_size; + bool rangefound = false; + t_value_holder *left = NULL; + t_value_holder *right = NULL; + for (i = 0; i < ARRAY_SIZE(sizes); i++) { + if (sizes[i].range == sizerange) { + result = talloc_zero(ctx, t_value_holder); + if (result == NULL) { + DBG_ERR("out of memory\n"); + return NULL; + } + lower_size = sizes[i].lower; + upper_size = sizes[i].upper; + rangefound = true; + break; + } + } + + if (!rangefound) { + return NULL; + } + + left = talloc_zero(ctx, t_value_holder); + + if (left == NULL) { + return NULL; + } + + left->type = NUMBER; + left->value.number = lower_size; + + if (upper_size) { + right = talloc_zero(ctx, t_value_holder); + if (right == NULL) { + return NULL; + } + right->type = NUMBER; + right->value.number = upper_size; + } + + result = create_value_range(ctx, left, right); + return result; +} diff --git a/libcli/wsp/wsp_aqs.h b/libcli/wsp/wsp_aqs.h new file mode 100644 index 00000000000..b34dd522d6d --- /dev/null +++ b/libcli/wsp/wsp_aqs.h @@ -0,0 +1,166 @@ +/* + * Window Search Service + * + * Copyright (c) Noel Power + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#ifndef __WSP_AQS_H__ +#define __WSP_AQS_H__ +#include "librpc/gen_ndr/wsp.h" + +typedef enum nodetype +{ + eAND, + eOR, + eNOT, + eVALUE, +} t_nodetype; + +typedef enum op +{ + eLT = PRLT, + eLE = PRLE, + eGT = PRGT, + eGE = PRGE, + eEQ = PREQ, + eNE = PRNE, + eSTARTSWITH, + eEQUALS, + /* + * eMATCHES, + * + * not sure we can express the above in the grammar without + * some custom operator :/ + */ +} t_optype; + +struct restr; + +typedef enum { + NUMBER, + STRING, + BOOL, + RESTR, + VALUE_RANGE, +} value_type; + +typedef enum { + eTODAY, + eYESTERDAY, + eLASTWEEK, + eTHISWEEK, + eTHISMONTH, + eLASTMONTH, + eTHISYEAR, + eLASTYEAR, +} daterange_type; + +typedef enum { + eEMPTY, + eTINY, + eSMALL, + eMEDIUM, + eLARGE, + eHUGE, + eGIGANTIC, +} sizerange_type; + +struct value_range; + +typedef struct { + value_type type; + union { + bool boolean; + const char *string; + uint64_t number; + struct restr *restr_tree; + struct value_range *value_range; + } value; +} t_value_holder; + +struct value_range +{ + t_value_holder *lower; + t_value_holder *upper; +}; +typedef struct basic_restr +{ + uint32_t prop_type; + t_optype op; + t_value_holder *values; +} t_basic_restr; + +typedef struct basic_query +{ + struct GUID guid; + const struct full_propset_info *prop_info; + char *prop; + t_basic_restr *basic_restriction; +} t_basic_query; + +t_basic_query *create_basic_query(TALLOC_CTX *ctx, const char *prop, t_basic_restr *restr); + +typedef struct restr +{ + t_nodetype type; + struct restr *left; + struct restr *right; + t_basic_restr *basic_restr; +} t_restr; + +t_restr *create_restr(TALLOC_CTX *ctx, t_nodetype op, t_restr *left, t_restr *right, t_basic_restr *value); + +t_basic_restr *create_basic_restr(TALLOC_CTX *ctx, + uint32_t prop_type, + t_optype op, + t_value_holder *values); + +typedef struct query +{ + t_nodetype type; + struct query *left; + struct query *right; + struct wsp_crestriction *restriction; +} t_query; + +t_query *create_query_node(TALLOC_CTX *ctx, t_nodetype op, t_query *left, t_query *right, t_basic_query *value); + + +typedef struct col_list { + int num_cols; + char **cols; +} t_col_list; + +typedef struct select_stmt { + t_col_list *cols; + t_query *where; +} t_select_stmt; + +t_col_list *create_cols(TALLOC_CTX *ctx, const char *col, t_col_list *append_list); +t_select_stmt *create_select(TALLOC_CTX *ctx, t_col_list *cols, t_query *where); + +t_select_stmt *get_wsp_sql_tree(const char *expr); +t_value_holder *create_string_val(TALLOC_CTX*, const char *text); +t_value_holder *create_num_val(TALLOC_CTX*, int64_t val); +t_value_holder *create_bool_val(TALLOC_CTX*, bool val); +t_value_holder *create_value_range(TALLOC_CTX*, + t_value_holder *left, + t_value_holder *right); +t_value_holder *create_date_range_shortcut(TALLOC_CTX *ctx, + daterange_type daterange); +t_value_holder *create_size_range_shortcut(TALLOC_CTX *ctx, + sizerange_type size); +#endif /* __WSP_AQS_H__ */ diff --git a/libcli/wsp/wsp_aqs_lexer.l b/libcli/wsp/wsp_aqs_lexer.l new file mode 100644 index 00000000000..dff4c1042b0 --- /dev/null +++ b/libcli/wsp/wsp_aqs_lexer.l @@ -0,0 +1,152 @@ +/* + * Unix SMB/CIFS implementation. + * + * Window Search Service + * + * Copyright (c) Noel Power + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +%{ + +#include "includes.h" +#include "libcli/wsp/wsp_aqs.h" +#include "libcli/wsp/wsp_aqs_parser.tab.h" + + +#include + +#define YY_NO_INPUT + +%} + +%option warn nodefault nounput + +%option reentrant noyywrap never-interactive nounistd +%option bison-bridge + +LPAREN "(" +RPAREN ")" +AND "AND" +OR "OR" +NOT "NOT" +EQ "==" +NE "!=" +GE ">=" +LE "<=" +LESS "<" +GREATER ">" +COMMA "," +WHERE "WHERE" +SELECT "SELECT" +PROP_EQUALS ":" +TRUE "true" +FALSE "false" + +TODAY "today" +YESTERDAY "yesterday" +THISWEEK "thisweek" +LASTWEEK "lastweek" +THISMONTH "thismonth" +LASTMONTH "lastmonth" +THISYEAR "thisyear" +LASTYEAR "lastyear" + +EMPTY "empty" +TINY "tiny" +SMALL "small" +MEDIUM "medium" +LARGE "large" +HUGE "huge" +GIGANTIC "gigantic" + +STARTS_WITH "$<" +EQUALS "$=" +K "K" +M "M" +G "G" +T "T" +KB "KB" +MB "MB" +GB "GB" +TB "TB" +RANGE "-" + + +NUMBER [0-9]+ +WS [ \r\n\t]* +IDENTIFIER [a-z\."A-Z_][a-z\."A-Z_0-9]* +STRING_LITERAL L?\"(\\.|[^\\"])*\" + +%% + +{WS} { /* Skip blanks. */ } + +{NUMBER} { sscanf(yytext, "%"PRId64, &yylval->num); return TOKEN_NUMBER; } + +{AND} { return TOKEN_AND; } +{OR} { return TOKEN_OR; } +{NOT} { return TOKEN_NOT; } +{EQ} { return TOKEN_EQ; } +{NE} { return TOKEN_NE; } +{GE} { return TOKEN_GE; } +{LE} { return TOKEN_LE; } +{LESS} { return TOKEN_LT; } +{GREATER} { return TOKEN_GT; } +{LPAREN} { return TOKEN_LPAREN; } +{RPAREN} { return TOKEN_RPAREN; } +{COMMA} { return TOKEN_COMMA; } +{WHERE} { return TOKEN_WHERE; } +{SELECT} { return TOKEN_SELECT; } +{TRUE} { return TOKEN_TRUE; } +{FALSE} { return TOKEN_FALSE; } +{PROP_EQUALS} { return TOKEN_PROP_EQUALS; } + +{STARTS_WITH} { return TOKEN_STARTS_WITH;} +{EQUALS} { return TOKEN_EQUALS;} + +{K} { return TOKEN_K; } +{M} { return TOKEN_M; } +{G} { return TOKEN_G; } +{T} { return TOKEN_T; } +{KB} { return TOKEN_KB; } +{MB} { return TOKEN_MB; } +{GB} { return TOKEN_GB; } +{TB} { return TOKEN_TB; } +{RANGE} { return TOKEN_RANGE; } +{TODAY} { return TOKEN_TODAY; } +{YESTERDAY} { return TOKEN_YESTERDAY;} +{THISWEEK} { return TOKEN_THISWEEK;} +{LASTWEEK} { return TOKEN_LASTWEEK;} +{THISMONTH} { return TOKEN_THISMONTH; } +{LASTMONTH} { return TOKEN_LASTMONTH; } +{THISYEAR} { return TOKEN_THISYEAR; } +{LASTYEAR} { return TOKEN_LASTYEAR; } +{EMPTY} { return TOKEN_EMPTY; } +{TINY} { return TOKEN_TINY; } +{SMALL} { return TOKEN_SMALL; } +{MEDIUM} { return TOKEN_MEDIUM; } +{LARGE} { return TOKEN_LARGE; } +{HUGE} { return TOKEN_HUGE; } +{GIGANTIC} { return TOKEN_GIGANTIC; } + + +{STRING_LITERAL} { yylval->strval = talloc_asprintf(talloc_tos(), "%s", yytext); return TOKEN_STRING_LITERAL; } + +{IDENTIFIER} { yylval->strval = talloc_asprintf(talloc_tos(), "%s", yytext); return TOKEN_IDENTIFIER; } +. { } + +%% + diff --git a/libcli/wsp/wsp_aqs_parser.y b/libcli/wsp/wsp_aqs_parser.y new file mode 100644 index 00000000000..2b0d7bd31c4 --- /dev/null +++ b/libcli/wsp/wsp_aqs_parser.y @@ -0,0 +1,422 @@ +/* + * Unix SMB/CIFS implementation. + * + * Window Search Service + * + * Copyright (c) Noel Power + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +%{ + +#include "includes.h" +#include "libcli/wsp/wsp_aqs.h" +#include "libcli/wsp/wsp_aqs_parser.tab.h" +#include "libcli/wsp/wsp_aqs_lexer.h" + +static int yyerror(t_select_stmt **stmt, yyscan_t scanner, const char *msg) +{ + fprintf(stderr,"Error :%s\n",msg); return 0; +} +%} +%code requires { + +#ifndef YY_TYPEDEF_YY_SCANNER_T +#define YY_TYPEDEF_YY_SCANNER_T +typedef void* yyscan_t; +#endif + +} + +%define api.pure +%lex-param { yyscan_t scanner } +%parse-param { t_select_stmt **select } +%parse-param { yyscan_t scanner } + +%union { + char *strval; + int64_t num; + t_value_holder *value; + t_select_stmt *select_stmt; + t_select_stmt *query_stmt; + t_basic_restr *bas_rest; + t_basic_query *bas_query; + t_restr *restr; + t_query *query; + t_col_list *columns; + daterange_type daterange; + sizerange_type sizerange; + t_optype prop_op; +} + +%left "AND" TOKEN_AND +%left "OR" TOKEN_OR +%left "!=" TOKEN_NE +%left ">=" TOKEN_GE +%left "<=" TOKEN_LE +%left "<" TOKEN_LT +%left ">" TOKEN_GT +%right "NOT" TOKEN_NOT +%right "==" TOKEN_EQ +%right ":" TOKEN_PROP_EQUALS + +%right "$<" TOKEN_STARTS_WITH +%right "$=" TOKEN_EQUALS + +%token TOKEN_LPAREN +%token TOKEN_RPAREN +%token TOKEN_AND +%token TOKEN_OR +%token TOKEN_WHERE +%token TOKEN_SELECT +%token TOKEN_TRUE +%token TOKEN_FALSE +%token TOKEN_COMMA +%token TOKEN_STARTS_WITH +%token TOKEN_EQUALS +%token TOKEN_MATCHES +%token TOKEN_K +%token TOKEN_M +%token TOKEN_G +%token TOKEN_T +%token TOKEN_KB +%token TOKEN_MB +%token TOKEN_GB +%token TOKEN_TB +%token TOKEN_RANGE +%token TOKEN_TODAY +%token TOKEN_YESTERDAY +%token TOKEN_THISWEEK +%token TOKEN_LASTWEEK +%token TOKEN_THISMONTH +%token TOKEN_LASTMONTH +%token TOKEN_THISYEAR +%token TOKEN_LASTYEAR +%token TOKEN_EMPTY +%token TOKEN_TINY +%token TOKEN_SMALL +%token TOKEN_MEDIUM +%token TOKEN_LARGE +%token TOKEN_HUGE +%token TOKEN_GIGANTIC + +%token TOKEN_NUMBER +%token TOKEN_IDENTIFIER +%token TOKEN_STRING_LITERAL + +%type prop +%type basic_restr +%type restr +%type basic_query +%type query +%type cols +%type col +%type select_stmt +%type simple_value +%type value +%type date_shortcut +%type property_op +%type content_op +%type size_shortcut + +%% + +input: + select_stmt { + *select = $1; + } +; + +select_stmt: + TOKEN_SELECT cols[C] TOKEN_WHERE query[Q] { + $$ = create_select(talloc_tos(), $C, $Q ); + if (!$$) { + YYERROR; + } + } + | query[Q] { + $$ = create_select(talloc_tos(), NULL, $Q ); + if (!$$) { + YYERROR; + } + } + ; + +cols : + col[C] { + $$ = create_cols(talloc_tos(), $1, NULL); + if (!$$) { + YYERROR; + } + } + | col[C] TOKEN_COMMA cols[CS] { + $$ = create_cols(talloc_tos(), $C, $CS); + if (!$$) { + YYERROR; + } + } + ; + +col: + TOKEN_IDENTIFIER[I] { + $$ = $I; + if (!$$) { + YYERROR; + } + } + ; + +query: + basic_query { + $$ = create_query_node(talloc_tos(), eVALUE, NULL, NULL, $1); + if (!$$) { + YYERROR; + } + } + | TOKEN_LPAREN query[Q] TOKEN_RPAREN { + $$ = $Q; + if (!$$) { + YYERROR; + } + } + | query[L] TOKEN_AND query[R] { + $$ = create_query_node(talloc_tos(), eAND, $L, $R, NULL); + if (!$$) { + YYERROR; + } + } + | query[L] TOKEN_OR query[R] { + $$ = create_query_node(talloc_tos(), eOR, $L, $R, NULL); + if (!$$) { + YYERROR; + } + } + | TOKEN_NOT query[R] { + $$ = create_query_node(talloc_tos(), eNOT, NULL, $R, NULL); + if (!$$) { + YYERROR; + } + } + ; + +basic_query: + prop[P] TOKEN_PROP_EQUALS basic_restr[V] { + $$ = create_basic_query(talloc_tos(), $P, $V); + if (!$$) { + YYERROR; + } + } + ; + +prop: TOKEN_IDENTIFIER[I] { + $$ = $I; + if (!$$) { + YYERROR; + } + } + ; + +basic_restr: + value[V] { + $$ = create_basic_restr(talloc_tos(), RTPROPERTY, eEQ, $V); + if (!$$) { + YYERROR; + } + } + | property_op[P] value[T] { + $$ = create_basic_restr(talloc_tos(), RTPROPERTY, $P, $T); + if (!$$) { + YYERROR; + } + } + | content_op[P] value[T] { + $$ = create_basic_restr(talloc_tos(), RTCONTENT, $P, $T); + if (!$$) { + YYERROR; + } + } + | TOKEN_LPAREN restr[R] TOKEN_RPAREN { + t_value_holder *holder = talloc_zero(talloc_tos(), t_value_holder); + holder->type = RESTR; + holder->value.restr_tree = $R; + $$ = create_basic_restr(talloc_tos(), RTNONE, eEQ, holder); + if (!$$) { + YYERROR; + } + } + ; + +property_op: + TOKEN_EQ { $$ = eEQ; } + | TOKEN_NE { $$ = eNE; } + | TOKEN_GE { $$ = eGE; } + | TOKEN_LE { $$ = eLE; } + | TOKEN_LT { $$ = eLT; } + | TOKEN_GT { $$ = eGT; } + ; + +content_op: + TOKEN_STARTS_WITH { $$ = eSTARTSWITH; } + | TOKEN_EQUALS { $$ = eEQUALS; } + ; + +value: + simple_value[V] { $$ = $V;} + | simple_value[L] TOKEN_RANGE simple_value[R] { + $$ = create_value_range(talloc_tos(), $L, $R); + if (!$$) { + YYERROR; + } + } + | date_shortcut[D] { + $$ = create_date_range_shortcut(talloc_tos(), $D); + if (!$$) { + YYERROR; + } + } + | size_shortcut[S] { + $$ = create_size_range_shortcut(talloc_tos(), $S); + if (!$$) { + YYERROR; + } + } + ; + +date_shortcut: + TOKEN_TODAY { $$ = eTODAY; } + | TOKEN_YESTERDAY { $$ = eYESTERDAY; } + | TOKEN_THISWEEK { $$ = eTHISWEEK; } + | TOKEN_LASTWEEK { $$ = eLASTWEEK; } + | TOKEN_THISMONTH { $$ = eTHISMONTH; } + | TOKEN_LASTMONTH { $$ = eTHISMONTH; } + | TOKEN_THISYEAR { $$ = eTHISYEAR; } + | TOKEN_LASTYEAR { $$ = eLASTYEAR; } + ; + +size_shortcut: + TOKEN_EMPTY { $$ = eEMPTY; } + | TOKEN_TINY { $$ = eTINY; } + | TOKEN_SMALL { $$ = eSMALL; } + | TOKEN_MEDIUM { $$ = eMEDIUM; } + | TOKEN_LARGE { $$ = eLARGE; } + | TOKEN_HUGE { $$ = eHUGE; } + | TOKEN_GIGANTIC { $$ = eGIGANTIC; } + ; + +simple_value: + TOKEN_NUMBER[N] { + $$ = create_num_val(talloc_tos(), $N); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_K { + $$ = create_num_val(talloc_tos(), $N * 1024); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_M { + $$ = create_num_val( talloc_tos(), $N * 1024 * 1024); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_G { + $$ = create_num_val(talloc_tos(), $N * 1024 * 1024 * 1024); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_T { + $$ = create_num_val(talloc_tos(), + $N * 1024 * 1024 * 1024 * 1024); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_KB { + $$ = create_num_val(talloc_tos(), $N * 1000); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_MB { + $$ = create_num_val( talloc_tos(), $N * 1000 * 1000); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_GB { + $$ = create_num_val(talloc_tos(), $N * 1000 * 1000 * 1000); + if (!$$) { + YYERROR; + } + } + | TOKEN_NUMBER[N] TOKEN_TB { + $$ = create_num_val(talloc_tos(), + $N * 1000 * 1000 * 1000 * 1000); + if (!$$) { + YYERROR; + } + } + | TOKEN_TRUE { + $$ = create_bool_val(talloc_tos(), true); + if (!$$) { + YYERROR; + } + } + | TOKEN_FALSE { + $$ = create_num_val(talloc_tos(), false); + if (!$$) { + YYERROR; + } + } + | TOKEN_STRING_LITERAL[S] { + char *tmp_str = talloc_strdup(talloc_tos(), $S+1); + tmp_str[strlen(tmp_str)-1] = '\0'; + $$ = create_string_val(talloc_tos(), tmp_str); + if (!$$) { + YYERROR; + } + } + | TOKEN_IDENTIFIER[I] { + $$ = create_string_val(talloc_tos(), $I); + if (!$$) { + YYERROR; + } + } + ; + +restr: basic_restr[V] { + $$ = create_restr(talloc_tos(), eVALUE, NULL, NULL, $V); + if (!$$) { + YYERROR; + } + } + | restr[L] TOKEN_AND restr[R] { + $$ = create_restr(talloc_tos(), eAND, $L, $R, NULL); + if (!$$) { + YYERROR; + } + } + | restr[L] TOKEN_OR restr[R] { + $$ = create_restr(talloc_tos(), eOR, $L, $R, NULL); + if (!$$) { + YYERROR; + } + } + ; +%% diff --git a/wscript_build b/wscript_build index 9379da5a098..dfdffb34c1f 100644 --- a/wscript_build +++ b/wscript_build @@ -103,6 +103,7 @@ bld.RECURSE('source4/libcli') bld.RECURSE('libcli/smb') bld.RECURSE('libcli/util') bld.RECURSE('libcli/cldap') +bld.RECURSE('libcli/wsp') bld.RECURSE('lib/smbconf') bld.RECURSE('lib/async_req') bld.RECURSE('lib/dbwrap') -- 2.34.1