| /* | |
| * HID descriptor parsing | |
| * | |
| * Copyright (C) 2015 Aric Stewart | |
| * | |
| * This library is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Lesser General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2.1 of the License, or (at your option) any later version. | |
| * | |
| * This library 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 | |
| * Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public | |
| * License along with this library; if not, write to the Free Software | |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA | |
| */ | |
| #include "config.h" | |
| #include <stdarg.h> | |
| #include <stdlib.h> | |
| #include <stdio.h> | |
| #define NONAMELESSUNION | |
| #include "hid.h" | |
| #include "wine/debug.h" | |
| #include "wine/list.h" | |
| WINE_DEFAULT_DEBUG_CHANNEL(hid); | |
| #define USAGE_MAX 10 | |
| /* Flags that are defined in the document | |
| "Device Class Definition for Human Interface Devices" */ | |
| enum { | |
| INPUT_DATA_CONST = 0x01, /* Data (0) | Constant (1) */ | |
| INPUT_ARRAY_VAR = 0x02, /* Array (0) | Variable (1) */ | |
| INPUT_ABS_REL = 0x04, /* Absolute (0) | Relative (1) */ | |
| INPUT_WRAP = 0x08, /* No Wrap (0) | Wrap (1) */ | |
| INPUT_LINEAR = 0x10, /* Linear (0) | Non Linear (1) */ | |
| INPUT_PREFSTATE = 0x20, /* Preferred State (0) | No Preferred (1) */ | |
| INPUT_NULL = 0x40, /* No Null position (0) | Null state(1) */ | |
| INPUT_VOLATILE = 0x80, /* Non Volatile (0) | Volatile (1) */ | |
| INPUT_BITFIELD = 0x100 /* Bit Field (0) | Buffered Bytes (1) */ | |
| }; | |
| enum { | |
| TAG_TYPE_MAIN = 0x0, | |
| TAG_TYPE_GLOBAL, | |
| TAG_TYPE_LOCAL, | |
| TAG_TYPE_RESERVED, | |
| }; | |
| enum { | |
| TAG_MAIN_INPUT = 0x08, | |
| TAG_MAIN_OUTPUT = 0x09, | |
| TAG_MAIN_FEATURE = 0x0B, | |
| TAG_MAIN_COLLECTION = 0x0A, | |
| TAG_MAIN_END_COLLECTION = 0x0C | |
| }; | |
| enum { | |
| TAG_GLOBAL_USAGE_PAGE = 0x0, | |
| TAG_GLOBAL_LOGICAL_MINIMUM, | |
| TAG_GLOBAL_LOGICAL_MAXIMUM, | |
| TAG_GLOBAL_PHYSICAL_MINIMUM, | |
| TAG_GLOBAL_PHYSICAL_MAXIMUM, | |
| TAG_GLOBAL_UNIT_EXPONENT, | |
| TAG_GLOBAL_UNIT, | |
| TAG_GLOBAL_REPORT_SIZE, | |
| TAG_GLOBAL_REPORT_ID, | |
| TAG_GLOBAL_REPORT_COUNT, | |
| TAG_GLOBAL_PUSH, | |
| TAG_GLOBAL_POP | |
| }; | |
| enum { | |
| TAG_LOCAL_USAGE = 0x0, | |
| TAG_LOCAL_USAGE_MINIMUM, | |
| TAG_LOCAL_USAGE_MAXIMUM, | |
| TAG_LOCAL_DESIGNATOR_INDEX, | |
| TAG_LOCAL_DESIGNATOR_MINIMUM, | |
| TAG_LOCAL_DESIGNATOR_MAXIMUM, | |
| TAG_LOCAL_STRING_INDEX, | |
| TAG_LOCAL_STRING_MINIMUM, | |
| TAG_LOCAL_STRING_MAXIMUM, | |
| TAG_LOCAL_DELIMITER | |
| }; | |
| static const char* const feature_string[] = | |
| { "Input", "Output", "Feature" }; | |
| struct caps { | |
| USAGE UsagePage; | |
| LONG LogicalMin; | |
| LONG LogicalMax; | |
| LONG PhysicalMin; | |
| LONG PhysicalMax; | |
| ULONG UnitsExp; | |
| ULONG Units; | |
| USHORT BitSize; | |
| UCHAR ReportID; | |
| USHORT ReportCount; | |
| BOOLEAN IsRange; | |
| BOOLEAN IsStringRange; | |
| BOOLEAN IsDesignatorRange; | |
| unsigned int usage_count; | |
| union { | |
| struct { | |
| USAGE UsageMin; | |
| USAGE UsageMax; | |
| USHORT StringMin; | |
| USHORT StringMax; | |
| USHORT DesignatorMin; | |
| USHORT DesignatorMax; | |
| } Range; | |
| struct { | |
| USAGE Usage[USAGE_MAX]; | |
| USAGE Reserved1; | |
| USHORT StringIndex; | |
| USHORT Reserved2; | |
| USHORT DesignatorIndex; | |
| USHORT Reserved3; | |
| } NotRange; | |
| } DUMMYUNIONNAME; | |
| int Delim; | |
| }; | |
| struct feature { | |
| struct list entry; | |
| struct list col_entry; | |
| struct caps caps; | |
| HIDP_REPORT_TYPE type; | |
| BOOLEAN isData; | |
| BOOLEAN isArray; | |
| BOOLEAN IsAbsolute; | |
| BOOLEAN Wrap; | |
| BOOLEAN Linear; | |
| BOOLEAN prefState; | |
| BOOLEAN HasNull; | |
| BOOLEAN Volatile; | |
| BOOLEAN BitField; | |
| unsigned int index; | |
| struct collection *collection; | |
| }; | |
| static const char* const collection_string[] = { | |
| "Physical", | |
| "Application", | |
| "Logical", | |
| "Report", | |
| "Named Array", | |
| "Usage Switch", | |
| "Usage Modifier", | |
| }; | |
| struct collection { | |
| struct list entry; | |
| struct caps caps; | |
| unsigned int index; | |
| unsigned int type; | |
| struct collection *parent; | |
| struct list features; | |
| struct list collections; | |
| }; | |
| struct caps_stack { | |
| struct list entry; | |
| struct caps caps; | |
| }; | |
| static const char* debugstr_usages(struct caps *caps) | |
| { | |
| if (!caps->IsRange) | |
| { | |
| char out[12 * USAGE_MAX]; | |
| unsigned int i; | |
| if (caps->usage_count == 0) | |
| return "[ none ]"; | |
| out[0] = 0; | |
| for (i = 0; i < caps->usage_count; i++) | |
| sprintf(out + strlen(out), "0x%x ", caps->u.NotRange.Usage[i]); | |
| return wine_dbg_sprintf("[ %s] ", out); | |
| } | |
| else | |
| return wine_dbg_sprintf("[0x%x - 0x%x]", caps->u.Range.UsageMin, caps->u.Range.UsageMax); | |
| } | |
| static const char* debugstr_stringindex(struct caps *caps) | |
| { | |
| if (!caps->IsStringRange) | |
| return wine_dbg_sprintf("%i", caps->u.NotRange.StringIndex); | |
| else | |
| return wine_dbg_sprintf("[%i - %i]", caps->u.Range.StringMin, caps->u.Range.StringMax); | |
| } | |
| static const char* debugstr_designatorindex(struct caps *caps) | |
| { | |
| if (!caps->IsDesignatorRange) | |
| return wine_dbg_sprintf("%i", caps->u.NotRange.DesignatorIndex); | |
| else | |
| return wine_dbg_sprintf("[%i - %i]", caps->u.Range.DesignatorMin, caps->u.Range.DesignatorMax); | |
| } | |
| static void debugstr_caps(const char* type, struct caps *caps) | |
| { | |
| if (!caps) | |
| return; | |
| TRACE("(%s Caps: UsagePage 0x%x; LogicalMin %i; LogicalMax %i; PhysicalMin %i; PhysicalMax %i; UnitsExp %i; Units %i; BitSize %i; ReportID %i; ReportCount %i; Usage %s; StringIndex %s; DesignatorIndex %s; Delim %i;)\n", | |
| type, | |
| caps->UsagePage, | |
| caps->LogicalMin, | |
| caps->LogicalMax, | |
| caps->PhysicalMin, | |
| caps->PhysicalMax, | |
| caps->UnitsExp, | |
| caps->Units, | |
| caps->BitSize, | |
| caps->ReportID, | |
| caps->ReportCount, | |
| debugstr_usages(caps), | |
| debugstr_stringindex(caps), | |
| debugstr_designatorindex(caps), | |
| caps->Delim); | |
| } | |
| static void debug_feature(struct feature *feature) | |
| { | |
| if (!feature) | |
| return; | |
| TRACE("[Feature type %s [%i]; %s; %s; %s; %s; %s; %s; %s; %s; %s]\n", | |
| feature_string[feature->type], | |
| feature->index, | |
| (feature->isData)?"Data":"Const", | |
| (feature->isArray)?"Array":"Var", | |
| (feature->IsAbsolute)?"Abs":"Rel", | |
| (feature->Wrap)?"Wrap":"NoWrap", | |
| (feature->Linear)?"Linear":"NonLinear", | |
| (feature->prefState)?"PrefStat":"NoPrefState", | |
| (feature->HasNull)?"HasNull":"NoNull", | |
| (feature->Volatile)?"Volatile":"NonVolatile", | |
| (feature->BitField)?"BitField":"Buffered"); | |
| debugstr_caps("Feature", &feature->caps); | |
| } | |
| static void debug_collection(struct collection *collection) | |
| { | |
| struct feature *fentry; | |
| struct collection *centry; | |
| if (TRACE_ON(hid)) | |
| { | |
| TRACE("START Collection %i <<< %s, parent: %p, %i features, %i collections\n", collection->index, collection_string[collection->type], collection->parent, list_count(&collection->features), list_count(&collection->collections)); | |
| debugstr_caps("Collection", &collection->caps); | |
| LIST_FOR_EACH_ENTRY(fentry, &collection->features, struct feature, col_entry) | |
| debug_feature(fentry); | |
| LIST_FOR_EACH_ENTRY(centry, &collection->collections, struct collection, entry) | |
| debug_collection(centry); | |
| TRACE(">>> END Collection %i\n", collection->index); | |
| } | |
| } | |
| static void debug_print_button_cap(const CHAR * type, WINE_HID_ELEMENT *wine_element) | |
| { | |
| if (!wine_element->caps.button.IsRange) | |
| TRACE("%s Button: 0x%x/0x%04x: ReportId %i, startBit %i/1\n" , type, | |
| wine_element->caps.button.UsagePage, | |
| wine_element->caps.button.u.NotRange.Usage, | |
| wine_element->caps.value.ReportID, | |
| wine_element->valueStartBit); | |
| else | |
| TRACE("%s Button: 0x%x/[0x%04x-0x%04x]: ReportId %i, startBit %i/%i\n" ,type, | |
| wine_element->caps.button.UsagePage, | |
| wine_element->caps.button.u.Range.UsageMin, | |
| wine_element->caps.button.u.Range.UsageMax, | |
| wine_element->caps.value.ReportID, | |
| wine_element->valueStartBit, | |
| wine_element->bitCount); | |
| } | |
| static void debug_print_value_cap(const CHAR * type, WINE_HID_ELEMENT *wine_element) | |
| { | |
| TRACE("%s Value: 0x%x/0x%x: ReportId %i, IsAbsolute %i, HasNull %i, " | |
| "Bit Size %i, ReportCount %i, UnitsExp %i, Units %i, " | |
| "LogicalMin %i, Logical Max %i, PhysicalMin %i, " | |
| "PhysicalMax %i -- StartBit %i/%i\n", type, | |
| wine_element->caps.value.UsagePage, | |
| wine_element->caps.value.u.NotRange.Usage, | |
| wine_element->caps.value.ReportID, | |
| wine_element->caps.value.IsAbsolute, | |
| wine_element->caps.value.HasNull, | |
| wine_element->caps.value.BitSize, | |
| wine_element->caps.value.ReportCount, | |
| wine_element->caps.value.UnitsExp, | |
| wine_element->caps.value.Units, | |
| wine_element->caps.value.LogicalMin, | |
| wine_element->caps.value.LogicalMax, | |
| wine_element->caps.value.PhysicalMin, | |
| wine_element->caps.value.PhysicalMax, | |
| wine_element->valueStartBit, | |
| wine_element->bitCount); | |
| } | |
| static void debug_print_element(const CHAR* type, WINE_HID_ELEMENT *wine_element) | |
| { | |
| if (wine_element->ElementType == ButtonElement) | |
| debug_print_button_cap(type, wine_element); | |
| else if (wine_element->ElementType == ValueElement) | |
| debug_print_value_cap(type, wine_element); | |
| else | |
| TRACE("%s: UNKNOWN\n", type); | |
| } | |
| static void debug_print_report(const char* type, WINE_HID_REPORT *report) | |
| { | |
| unsigned int i; | |
| TRACE("START Report %i <<< %s report : dwSize: %i elementCount: %i\n", | |
| report->reportID, | |
| type, | |
| report->dwSize, | |
| report->elementCount); | |
| for (i = 0; i < report->elementCount; i++) | |
| debug_print_element(type, &report->Elements[i]); | |
| TRACE(">>> END Report %i\n",report->reportID); | |
| } | |
| static void debug_print_preparsed(WINE_HIDP_PREPARSED_DATA *data) | |
| { | |
| unsigned int i; | |
| WINE_HID_REPORT *r; | |
| if (TRACE_ON(hid)) | |
| { | |
| TRACE("START PREPARSED Data <<< dwSize: %i Usage: %i, UsagePage: %i, InputReportByteLength: %i, tOutputReportByteLength: %i, FeatureReportByteLength: %i, NumberLinkCollectionNodes: %i, NumberInputButtonCaps: %i, NumberInputValueCaps: %i,NumberInputDataIndices: %i, NumberOutputButtonCaps: %i, NumberOutputValueCaps: %i, NumberOutputDataIndices: %i, NumberFeatureButtonCaps: %i, NumberFeatureValueCaps: %i, NumberFeatureDataIndices: %i, dwInputReportCount: %i, dwOutputReportCount: %i, dwFeatureReportCount: %i, dwOutputReportOffset: %i, dwFeatureReportOffset: %i\n", | |
| data->dwSize, | |
| data->caps.Usage, | |
| data->caps.UsagePage, | |
| data->caps.InputReportByteLength, | |
| data->caps.OutputReportByteLength, | |
| data->caps.FeatureReportByteLength, | |
| data->caps.NumberLinkCollectionNodes, | |
| data->caps.NumberInputButtonCaps, | |
| data->caps.NumberInputValueCaps, | |
| data->caps.NumberInputDataIndices, | |
| data->caps.NumberOutputButtonCaps, | |
| data->caps.NumberOutputValueCaps, | |
| data->caps.NumberOutputDataIndices, | |
| data->caps.NumberFeatureButtonCaps, | |
| data->caps.NumberFeatureValueCaps, | |
| data->caps.NumberFeatureDataIndices, | |
| data->dwInputReportCount, | |
| data->dwOutputReportCount, | |
| data->dwFeatureReportCount, | |
| data->dwOutputReportOffset, | |
| data->dwFeatureReportOffset); | |
| r = HID_INPUT_REPORTS(data); | |
| for (i = 0; i < data->dwInputReportCount; i++) | |
| { | |
| debug_print_report("INPUT", r); | |
| r = HID_NEXT_REPORT(data, r); | |
| } | |
| r = HID_OUTPUT_REPORTS(data); | |
| for (i = 0; i < data->dwOutputReportCount; i++) | |
| { | |
| debug_print_report("OUTPUT", r); | |
| r = HID_NEXT_REPORT(data, r); | |
| } | |
| r = HID_FEATURE_REPORTS(data); | |
| for (i = 0; i < data->dwFeatureReportCount; i++) | |
| { | |
| debug_print_report("FEATURE", r); | |
| r = HID_NEXT_REPORT(data, r); | |
| } | |
| TRACE(">>> END Preparsed Data\n"); | |
| } | |
| } | |
| static int getValue(int bsize, int source, BOOL allow_negative) | |
| { | |
| int mask = 0xff; | |
| int negative = 0x80; | |
| int outofrange = 0x100; | |
| int value; | |
| unsigned int i; | |
| if (bsize == 4) | |
| return source; | |
| for (i = 1; i < bsize; i++) | |
| { | |
| mask = (mask<<8) + 0xff; | |
| negative = (negative<<8); | |
| outofrange = (outofrange<<8); | |
| } | |
| value = (source&mask); | |
| if (allow_negative && value&negative) | |
| value = -1 * (outofrange - value); | |
| return value; | |
| } | |
| static void parse_io_feature(unsigned int bSize, int itemVal, int bTag, | |
| unsigned int *feature_index, | |
| struct feature *feature) | |
| { | |
| if (bSize == 0) | |
| { | |
| return; | |
| } | |
| else | |
| { | |
| feature->isData = ((itemVal & INPUT_DATA_CONST) == 0); | |
| feature->isArray = ((itemVal & INPUT_ARRAY_VAR) == 0); | |
| feature->IsAbsolute = ((itemVal & INPUT_ABS_REL) == 0); | |
| feature->Wrap = ((itemVal & INPUT_WRAP) != 0); | |
| feature->Linear = ((itemVal & INPUT_LINEAR) == 0); | |
| feature->prefState = ((itemVal & INPUT_PREFSTATE) == 0); | |
| feature->HasNull = ((itemVal & INPUT_NULL) != 0); | |
| if (bTag != TAG_MAIN_INPUT) | |
| { | |
| feature->Volatile = ((itemVal & INPUT_VOLATILE) != 0); | |
| } | |
| if (bSize > 1) | |
| { | |
| feature->BitField = ((itemVal & INPUT_BITFIELD) == 0); | |
| } | |
| feature->index = *feature_index; | |
| *feature_index = *feature_index + 1; | |
| } | |
| } | |
| static void parse_collection(unsigned int bSize, int itemVal, | |
| struct collection *collection) | |
| { | |
| if (bSize <= 0) | |
| return; | |
| else | |
| { | |
| collection->type = itemVal; | |
| if (itemVal >= 0x07 && itemVal <= 0x7F) { | |
| ERR(" (Reserved 0x%x )\n", itemVal); | |
| } | |
| else if (itemVal >= 0x80 && itemVal <= 0xFF) { | |
| ERR(" (Vendor Defined 0x%x )\n", itemVal); | |
| } | |
| } | |
| } | |
| static void new_caps(struct caps *caps) | |
| { | |
| caps->IsRange = 0; | |
| caps->IsStringRange = 0; | |
| caps->IsDesignatorRange = 0; | |
| caps->usage_count = 0; | |
| } | |
| static int parse_descriptor(BYTE *descriptor, unsigned int index, unsigned int length, | |
| unsigned int *feature_index, unsigned int *collection_index, | |
| struct collection *collection, struct caps *caps, | |
| struct list *features, struct list *stack) | |
| { | |
| unsigned int i; | |
| for (i = index; i < length;) | |
| { | |
| BYTE b0 = descriptor[i++]; | |
| int bSize = b0 & 0x03; | |
| int bType = (b0 >> 2) & 0x03; | |
| int bTag = (b0 >> 4) & 0x0F; | |
| bSize = (bSize == 3) ? 4 : bSize; | |
| if (bType == TAG_TYPE_RESERVED && bTag == 0x0F && bSize == 2 && | |
| i + 2 < length) | |
| { | |
| /* Long data items: Should be unused */ | |
| ERR("Long Data Item, should be unused\n"); | |
| } | |
| else | |
| { | |
| int bSizeActual = 0; | |
| int itemVal = 0; | |
| unsigned int j; | |
| for (j = 0; j < bSize; j++) | |
| { | |
| if (i + j < length) | |
| { | |
| itemVal += descriptor[i + j] << (8 * j); | |
| bSizeActual++; | |
| } | |
| } | |
| TRACE(" 0x%x[%i], type %i , tag %i, size %i, val %i\n",b0,i-1,bType, bTag, bSize, itemVal ); | |
| if (bType == TAG_TYPE_MAIN) | |
| { | |
| struct feature *feature; | |
| switch(bTag) | |
| { | |
| case TAG_MAIN_INPUT: | |
| feature = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*feature)); | |
| list_add_tail(&collection->features, &feature->col_entry); | |
| list_add_tail(features, &feature->entry); | |
| feature->type = HidP_Input; | |
| parse_io_feature(bSize, itemVal, bTag, feature_index, feature); | |
| feature->caps = *caps; | |
| feature->collection = collection; | |
| new_caps(caps); | |
| break; | |
| case TAG_MAIN_OUTPUT: | |
| feature = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*feature)); | |
| list_add_tail(&collection->features, &feature->col_entry); | |
| list_add_tail(features, &feature->entry); | |
| feature->type = HidP_Output; | |
| parse_io_feature(bSize, itemVal, bTag, feature_index, feature); | |
| feature->caps = *caps; | |
| feature->collection = collection; | |
| new_caps(caps); | |
| break; | |
| case TAG_MAIN_FEATURE: | |
| feature = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*feature)); | |
| list_add_tail(&collection->features, &feature->col_entry); | |
| list_add_tail(features, &feature->entry); | |
| feature->type = HidP_Feature; | |
| parse_io_feature(bSize, itemVal, bTag, feature_index, feature); | |
| feature->caps = *caps; | |
| feature->collection = collection; | |
| new_caps(caps); | |
| break; | |
| case TAG_MAIN_COLLECTION: | |
| { | |
| struct collection *subcollection = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(struct collection)); | |
| list_add_tail(&collection->collections, &subcollection->entry); | |
| subcollection->parent = collection; | |
| /* Only set our collection once... | |
| We do not properly handle composite devices yet. */ | |
| if (*collection_index == 0) | |
| collection->caps = *caps; | |
| subcollection->caps = *caps; | |
| subcollection->index = *collection_index; | |
| *collection_index = *collection_index + 1; | |
| list_init(&subcollection->features); | |
| list_init(&subcollection->collections); | |
| new_caps(caps); | |
| parse_collection(bSize, itemVal, subcollection); | |
| i = parse_descriptor(descriptor, i+1, length, feature_index, collection_index, subcollection, caps, features, stack); | |
| continue; | |
| } | |
| case TAG_MAIN_END_COLLECTION: | |
| return i; | |
| default: | |
| ERR("Unknown (bTag: 0x%x, bType: 0x%x)\n", bTag, bType); | |
| } | |
| } | |
| else if (bType == TAG_TYPE_GLOBAL) | |
| { | |
| switch(bTag) | |
| { | |
| case TAG_GLOBAL_USAGE_PAGE: | |
| caps->UsagePage = getValue(bSize, itemVal, FALSE); | |
| break; | |
| case TAG_GLOBAL_LOGICAL_MINIMUM: | |
| caps->LogicalMin = getValue(bSize, itemVal, TRUE); | |
| break; | |
| case TAG_GLOBAL_LOGICAL_MAXIMUM: | |
| caps->LogicalMax = getValue(bSize, itemVal, TRUE); | |
| break; | |
| case TAG_GLOBAL_PHYSICAL_MINIMUM: | |
| caps->PhysicalMin = getValue(bSize, itemVal, TRUE); | |
| break; | |
| case TAG_GLOBAL_PHYSICAL_MAXIMUM: | |
| caps->PhysicalMax = getValue(bSize, itemVal, TRUE); | |
| break; | |
| case TAG_GLOBAL_UNIT_EXPONENT: | |
| caps->UnitsExp = getValue(bSize, itemVal, TRUE); | |
| break; | |
| case TAG_GLOBAL_UNIT: | |
| caps->Units = getValue(bSize, itemVal, TRUE); | |
| break; | |
| case TAG_GLOBAL_REPORT_SIZE: | |
| caps->BitSize = getValue(bSize, itemVal, FALSE); | |
| break; | |
| case TAG_GLOBAL_REPORT_ID: | |
| caps->ReportID = getValue(bSize, itemVal, FALSE); | |
| break; | |
| case TAG_GLOBAL_REPORT_COUNT: | |
| caps->ReportCount = getValue(bSize, itemVal, FALSE); | |
| break; | |
| case TAG_GLOBAL_PUSH: | |
| { | |
| struct caps_stack *saved = HeapAlloc(GetProcessHeap(), 0, sizeof(*saved)); | |
| saved->caps = *caps; | |
| TRACE("Push\n"); | |
| list_add_tail(stack, &saved->entry); | |
| break; | |
| } | |
| case TAG_GLOBAL_POP: | |
| { | |
| struct list *tail; | |
| struct caps_stack *saved; | |
| TRACE("Pop\n"); | |
| tail = list_tail(stack); | |
| if (tail) | |
| { | |
| saved = LIST_ENTRY(tail, struct caps_stack, entry); | |
| *caps = saved->caps; | |
| list_remove(tail); | |
| HeapFree(GetProcessHeap(), 0, saved); | |
| } | |
| else | |
| ERR("Pop but no stack!\n"); | |
| break; | |
| } | |
| default: | |
| ERR("Unknown (bTag: 0x%x, bType: 0x%x)\n", bTag, bType); | |
| } | |
| } | |
| else if (bType == TAG_TYPE_LOCAL) | |
| { | |
| switch(bTag) | |
| { | |
| case TAG_LOCAL_USAGE: | |
| if (caps->usage_count >= USAGE_MAX) | |
| FIXME("More than %i individual usages defined\n",USAGE_MAX); | |
| else | |
| { | |
| caps->u.NotRange.Usage[caps->usage_count++] = getValue(bSize, itemVal, FALSE); | |
| caps->IsRange = FALSE; | |
| } | |
| break; | |
| case TAG_LOCAL_USAGE_MINIMUM: | |
| caps->usage_count = 1; | |
| caps->u.Range.UsageMin = getValue(bSize, itemVal, FALSE); | |
| caps->IsRange = TRUE; | |
| break; | |
| case TAG_LOCAL_USAGE_MAXIMUM: | |
| caps->usage_count = 1; | |
| caps->u.Range.UsageMax = getValue(bSize, itemVal, FALSE); | |
| caps->IsRange = TRUE; | |
| break; | |
| case TAG_LOCAL_DESIGNATOR_INDEX: | |
| caps->u.NotRange.DesignatorIndex = getValue(bSize, itemVal, FALSE); | |
| caps->IsDesignatorRange = FALSE; | |
| break; | |
| case TAG_LOCAL_DESIGNATOR_MINIMUM: | |
| caps->u.Range.DesignatorMin = getValue(bSize, itemVal, FALSE); | |
| caps->IsDesignatorRange = TRUE; | |
| break; | |
| case TAG_LOCAL_DESIGNATOR_MAXIMUM: | |
| caps->u.Range.DesignatorMax = getValue(bSize, itemVal, FALSE); | |
| caps->IsDesignatorRange = TRUE; | |
| break; | |
| case TAG_LOCAL_STRING_INDEX: | |
| caps->u.NotRange.StringIndex = getValue(bSize, itemVal, FALSE); | |
| caps->IsStringRange = FALSE; | |
| break; | |
| case TAG_LOCAL_STRING_MINIMUM: | |
| caps->u.Range.StringMin = getValue(bSize, itemVal, FALSE); | |
| caps->IsStringRange = TRUE; | |
| break; | |
| case TAG_LOCAL_STRING_MAXIMUM: | |
| caps->u.Range.StringMax = getValue(bSize, itemVal, FALSE); | |
| caps->IsStringRange = TRUE; | |
| break; | |
| case TAG_LOCAL_DELIMITER: | |
| caps->Delim = getValue(bSize, itemVal, FALSE); | |
| break; | |
| default: | |
| ERR("Unknown (bTag: 0x%x, bType: 0x%x)\n", bTag, bType); | |
| } | |
| } | |
| else | |
| ERR("Unknown (bTag: 0x%x, bType: 0x%x)\n", bTag, bType); | |
| i += bSize; | |
| } | |
| } | |
| return i; | |
| } | |
| static inline void new_report(WINE_HID_REPORT *wine_report, struct feature* feature) | |
| { | |
| wine_report->reportID = feature->caps.ReportID; | |
| wine_report->dwSize = sizeof(*wine_report) - sizeof(WINE_HID_ELEMENT); | |
| wine_report->elementCount = 0; | |
| } | |
| static void build_elements(WINE_HID_REPORT *wine_report, struct feature* feature, DWORD *bitOffset, unsigned int *data_index) | |
| { | |
| unsigned int i; | |
| if (!feature->isData) | |
| { | |
| *bitOffset = *bitOffset + (feature->caps.BitSize * feature->caps.ReportCount); | |
| return; | |
| } | |
| for (i = 0; i < feature->caps.usage_count; i++) | |
| { | |
| WINE_HID_ELEMENT *wine_element = &wine_report->Elements[wine_report->elementCount]; | |
| wine_element->valueStartBit = *bitOffset; | |
| if (feature->caps.UsagePage == HID_USAGE_PAGE_BUTTON) | |
| { | |
| wine_element->ElementType = ButtonElement; | |
| wine_element->caps.button.UsagePage = feature->caps.UsagePage; | |
| wine_element->caps.button.ReportID = feature->caps.ReportID; | |
| wine_element->caps.button.BitField = feature->BitField; | |
| wine_element->caps.button.IsRange = feature->caps.IsRange; | |
| wine_element->caps.button.IsStringRange = feature->caps.IsStringRange; | |
| wine_element->caps.button.IsDesignatorRange = feature->caps.IsDesignatorRange; | |
| wine_element->caps.button.IsAbsolute = feature->IsAbsolute; | |
| if (wine_element->caps.button.IsRange) | |
| { | |
| wine_element->bitCount = (feature->caps.u.Range.UsageMax - feature->caps.u.Range.UsageMin) + 1; | |
| *bitOffset = *bitOffset + wine_element->bitCount; | |
| wine_element->caps.button.u.Range.UsageMin = feature->caps.u.Range.UsageMin; | |
| wine_element->caps.button.u.Range.UsageMax = feature->caps.u.Range.UsageMax; | |
| wine_element->caps.button.u.Range.StringMin = feature->caps.u.Range.StringMin; | |
| wine_element->caps.button.u.Range.StringMax = feature->caps.u.Range.StringMax; | |
| wine_element->caps.button.u.Range.DesignatorMin = feature->caps.u.Range.DesignatorMin; | |
| wine_element->caps.button.u.Range.DesignatorMax = feature->caps.u.Range.DesignatorMax; | |
| wine_element->caps.button.u.Range.DataIndexMin = *data_index; | |
| wine_element->caps.button.u.Range.DataIndexMax = *data_index + wine_element->bitCount - 1; | |
| *data_index = *data_index + wine_element->bitCount; | |
| } | |
| else | |
| { | |
| *bitOffset = *bitOffset + 1; | |
| wine_element->bitCount = 1; | |
| wine_element->caps.button.u.NotRange.Usage = feature->caps.u.NotRange.Usage[i]; | |
| wine_element->caps.button.u.NotRange.StringIndex = feature->caps.u.NotRange.StringIndex; | |
| wine_element->caps.button.u.NotRange.DesignatorIndex = feature->caps.u.NotRange.DesignatorIndex; | |
| wine_element->caps.button.u.NotRange.DataIndex = *data_index; | |
| *data_index = *data_index + 1; | |
| } | |
| } | |
| else | |
| { | |
| wine_element->ElementType = ValueElement; | |
| wine_element->caps.value.UsagePage = feature->caps.UsagePage; | |
| wine_element->caps.value.ReportID = feature->caps.ReportID; | |
| wine_element->caps.value.BitField = feature->BitField; | |
| wine_element->caps.value.IsRange = feature->caps.IsRange; | |
| wine_element->caps.value.IsStringRange = feature->caps.IsStringRange; | |
| wine_element->caps.value.IsDesignatorRange = feature->caps.IsDesignatorRange; | |
| wine_element->caps.value.IsAbsolute = feature->IsAbsolute; | |
| wine_element->caps.value.HasNull = feature->HasNull; | |
| wine_element->caps.value.BitSize = feature->caps.BitSize; | |
| if (feature->caps.usage_count > 1) | |
| { | |
| if (feature->caps.ReportCount > feature->caps.usage_count) | |
| wine_element->caps.value.ReportCount = feature->caps.ReportCount / feature->caps.usage_count; | |
| else | |
| wine_element->caps.value.ReportCount = 1; | |
| } | |
| else | |
| wine_element->caps.value.ReportCount = feature->caps.ReportCount; | |
| wine_element->bitCount = (feature->caps.BitSize * wine_element->caps.value.ReportCount); | |
| *bitOffset = *bitOffset + wine_element->bitCount; | |
| wine_element->caps.value.UnitsExp = feature->caps.UnitsExp; | |
| wine_element->caps.value.Units = feature->caps.Units; | |
| wine_element->caps.value.LogicalMin = feature->caps.LogicalMin; | |
| wine_element->caps.value.LogicalMax = feature->caps.LogicalMax; | |
| wine_element->caps.value.PhysicalMin = feature->caps.PhysicalMin; | |
| wine_element->caps.value.PhysicalMax = feature->caps.PhysicalMax; | |
| if (wine_element->caps.value.IsRange) | |
| { | |
| wine_element->caps.value.u.Range.UsageMin = feature->caps.u.Range.UsageMin; | |
| wine_element->caps.value.u.Range.UsageMax = feature->caps.u.Range.UsageMax; | |
| wine_element->caps.value.u.Range.StringMin = feature->caps.u.Range.StringMin; | |
| wine_element->caps.value.u.Range.StringMax = feature->caps.u.Range.StringMax; | |
| wine_element->caps.value.u.Range.DesignatorMin = feature->caps.u.Range.DesignatorMin; | |
| wine_element->caps.value.u.Range.DesignatorMax = feature->caps.u.Range.DesignatorMax; | |
| wine_element->caps.value.u.Range.DataIndexMin = *data_index; | |
| wine_element->caps.value.u.Range.DataIndexMax = *data_index + | |
| (wine_element->caps.value.u.Range.UsageMax - | |
| wine_element->caps.value.u.Range.UsageMin); | |
| *data_index = *data_index + | |
| (wine_element->caps.value.u.Range.UsageMax - | |
| wine_element->caps.value.u.Range.UsageMin) + 1; | |
| } | |
| else | |
| { | |
| wine_element->caps.value.u.NotRange.Usage = feature->caps.u.NotRange.Usage[i]; | |
| wine_element->caps.value.u.NotRange.StringIndex = feature->caps.u.NotRange.StringIndex; | |
| wine_element->caps.value.u.NotRange.DesignatorIndex = feature->caps.u.NotRange.DesignatorIndex; | |
| wine_element->caps.value.u.NotRange.DataIndex = *data_index; | |
| *data_index = *data_index + 1; | |
| } | |
| } | |
| wine_report->elementCount++; | |
| } | |
| } | |
| static void count_elements(struct feature* feature, USHORT *buttons, USHORT *values) | |
| { | |
| if (feature->caps.UsagePage == HID_USAGE_PAGE_BUTTON) | |
| { | |
| if (feature->caps.IsRange) | |
| *buttons = *buttons + 1; | |
| else | |
| *buttons = *buttons + feature->caps.usage_count; | |
| } | |
| else | |
| { | |
| if (feature->caps.IsRange) | |
| *values = *values + 1; | |
| else | |
| *values = *values + feature->caps.usage_count; | |
| } | |
| } | |
| static WINE_HIDP_PREPARSED_DATA* build_PreparseData( | |
| struct feature **features, int feature_count, | |
| struct feature **input_features, int i_count, | |
| struct feature **output_features, int o_count, | |
| struct feature **feature_features, int f_count, | |
| struct collection *base_collection) | |
| { | |
| WINE_HIDP_PREPARSED_DATA *data; | |
| WINE_HID_REPORT *wine_report = NULL; | |
| DWORD bitOffset, bitLength; | |
| unsigned int report_count = 1; | |
| unsigned int i; | |
| unsigned int element_count; | |
| unsigned int size = 0; | |
| unsigned int data_index; | |
| if (features[0]->caps.ReportID != 0) | |
| { | |
| unsigned int *report_ids; | |
| unsigned int cnt = max(i_count, o_count); | |
| cnt = max(cnt, f_count); | |
| report_ids = HeapAlloc(GetProcessHeap(), 0 , sizeof(*report_ids) * cnt); | |
| if (i_count) | |
| { | |
| report_ids[0] = input_features[0]->caps.ReportID; | |
| for (i = 1; i < i_count; i++) | |
| { | |
| unsigned int j; | |
| unsigned int found = FALSE; | |
| for (j = 0; !found && j < i_count; j++) | |
| { | |
| if (report_ids[j] == input_features[i]->caps.ReportID) | |
| found = TRUE; | |
| } | |
| if (!found) | |
| { | |
| report_ids[report_count] = input_features[i]->caps.ReportID; | |
| report_count++; | |
| } | |
| } | |
| } | |
| if (o_count) | |
| { | |
| report_count++; | |
| report_ids[0] = output_features[0]->caps.ReportID; | |
| for (i = 1; i < o_count; i++) | |
| { | |
| unsigned int j; | |
| unsigned int found = FALSE; | |
| for (j = 0; !found && j < o_count; j++) | |
| { | |
| if (report_ids[j] == output_features[i]->caps.ReportID) | |
| found = TRUE; | |
| } | |
| if (!found) | |
| { | |
| report_ids[report_count] = output_features[i]->caps.ReportID; | |
| report_count++; | |
| } | |
| } | |
| } | |
| if (f_count) | |
| { | |
| report_count++; | |
| report_ids[0] = feature_features[0]->caps.ReportID; | |
| for (i = 1; i < f_count; i++) | |
| { | |
| unsigned int j; | |
| unsigned int found = FALSE; | |
| for (j = 0; !found && j < f_count; j++) | |
| { | |
| if (report_ids[j] == feature_features[i]->caps.ReportID) | |
| found = TRUE; | |
| } | |
| if (!found) | |
| { | |
| report_ids[report_count] = feature_features[i]->caps.ReportID; | |
| report_count++; | |
| } | |
| } | |
| } | |
| HeapFree(GetProcessHeap(), 0, report_ids); | |
| } | |
| else | |
| { | |
| if (o_count) report_count++; | |
| if (f_count) report_count++; | |
| } | |
| element_count = 0; | |
| for (i = 0; i < feature_count; i++) | |
| element_count += features[i]->caps.usage_count; | |
| size = sizeof(WINE_HIDP_PREPARSED_DATA) + | |
| (element_count * sizeof(WINE_HID_ELEMENT)) + | |
| (report_count * sizeof(WINE_HID_REPORT)); | |
| TRACE("%i reports %i elements -> size %i\n",report_count, element_count, size); | |
| data = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size); | |
| data->magic = HID_MAGIC; | |
| data->dwSize = size; | |
| data->caps.Usage = base_collection->caps.u.NotRange.Usage[0]; | |
| data->caps.UsagePage = base_collection->caps.UsagePage; | |
| wine_report = data->InputReports; | |
| if (i_count) | |
| { | |
| data_index = 0; | |
| bitLength = 0; | |
| new_report(wine_report, input_features[0]); | |
| data->dwInputReportCount++; | |
| /* Room for the reportID */ | |
| bitOffset = 8; | |
| for (i = 0; i < i_count; i++) | |
| { | |
| if (input_features[i]->caps.ReportID != wine_report->reportID) | |
| { | |
| wine_report->dwSize += (sizeof(WINE_HID_ELEMENT) * wine_report->elementCount); | |
| wine_report = (WINE_HID_REPORT*)(((BYTE*)wine_report)+wine_report->dwSize); | |
| new_report(wine_report, input_features[i]); | |
| data->dwInputReportCount++; | |
| bitLength = max(bitOffset, bitLength); | |
| bitOffset = 8; | |
| } | |
| build_elements(wine_report, input_features[i], &bitOffset, &data_index); | |
| count_elements(input_features[i], &data->caps.NumberInputButtonCaps, | |
| &data->caps.NumberInputValueCaps); | |
| } | |
| wine_report->dwSize += (sizeof(WINE_HID_ELEMENT) * wine_report->elementCount); | |
| bitLength = max(bitOffset, bitLength); | |
| data->caps.InputReportByteLength = ((bitLength + 7) & ~7)/8; | |
| data->caps.NumberInputDataIndices = data_index; | |
| } | |
| if (o_count) | |
| { | |
| data_index = 0; | |
| bitLength = 0; | |
| wine_report = (WINE_HID_REPORT*)(((BYTE*)wine_report)+wine_report->dwSize); | |
| data->dwOutputReportOffset = (BYTE*)wine_report - (BYTE*)data->InputReports; | |
| new_report(wine_report, output_features[0]); | |
| data->dwOutputReportCount++; | |
| bitOffset = 8; | |
| for (i = 0; i < o_count; i++) | |
| { | |
| if (output_features[i]->caps.ReportID != wine_report->reportID) | |
| { | |
| wine_report->dwSize += (sizeof(WINE_HID_ELEMENT) * wine_report->elementCount); | |
| wine_report = (WINE_HID_REPORT*)(((BYTE*)wine_report)+wine_report->dwSize); | |
| new_report(wine_report, output_features[i]); | |
| data->dwOutputReportCount++; | |
| bitLength = max(bitOffset, bitLength); | |
| bitOffset = 8; | |
| } | |
| build_elements(wine_report, output_features[i], &bitOffset, &data_index); | |
| count_elements(output_features[i], &data->caps.NumberOutputButtonCaps, | |
| &data->caps.NumberOutputValueCaps); | |
| } | |
| wine_report->dwSize += (sizeof(WINE_HID_ELEMENT) * wine_report->elementCount); | |
| bitLength = max(bitOffset, bitLength); | |
| data->caps.OutputReportByteLength = ((bitLength + 7) & ~7)/8; | |
| data->caps.NumberOutputDataIndices = data_index; | |
| } | |
| if (f_count) | |
| { | |
| data_index = 0; | |
| bitLength = 0; | |
| wine_report = (WINE_HID_REPORT*)(((BYTE*)wine_report)+wine_report->dwSize); | |
| data->dwFeatureReportOffset = (BYTE*)wine_report - (BYTE*)data->InputReports; | |
| new_report(wine_report, feature_features[0]); | |
| data->dwFeatureReportCount++; | |
| bitOffset = 8; | |
| for (i = 0; i < f_count; i++) | |
| { | |
| if (feature_features[i]->caps.ReportID != wine_report->reportID) | |
| { | |
| wine_report->dwSize += (sizeof(WINE_HID_ELEMENT) * wine_report->elementCount); | |
| wine_report = (WINE_HID_REPORT*)((BYTE*)wine_report+wine_report->dwSize); | |
| new_report(wine_report, feature_features[i]); | |
| data->dwFeatureReportCount++; | |
| bitLength = max(bitOffset, bitLength); | |
| bitOffset = 8; | |
| } | |
| build_elements(wine_report, feature_features[i], &bitOffset, &data_index); | |
| count_elements(feature_features[i], &data->caps.NumberFeatureButtonCaps, | |
| &data->caps.NumberFeatureValueCaps); | |
| } | |
| bitLength = max(bitOffset, bitLength); | |
| data->caps.FeatureReportByteLength = ((bitLength + 7) & ~7)/8; | |
| data->caps.NumberFeatureDataIndices = data_index; | |
| } | |
| return data; | |
| } | |
| static void free_collection(struct collection *collection) | |
| { | |
| struct feature *fentry, *fnext; | |
| struct collection *centry, *cnext; | |
| LIST_FOR_EACH_ENTRY_SAFE(centry, cnext, &collection->collections, struct collection, entry) | |
| { | |
| list_remove(¢ry->entry); | |
| free_collection(centry); | |
| } | |
| LIST_FOR_EACH_ENTRY_SAFE(fentry, fnext, &collection->features, struct feature, col_entry) | |
| { | |
| list_remove(&fentry->col_entry); | |
| HeapFree(GetProcessHeap(), 0, fentry); | |
| } | |
| HeapFree(GetProcessHeap(), 0, collection); | |
| } | |
| static int compare_reports(const void *a, const void* b) | |
| { | |
| struct feature *f1 = *(struct feature **)a; | |
| struct feature *f2 = *(struct feature **)b; | |
| int c = (f1->caps.ReportID - f2->caps.ReportID); | |
| if (c) return c; | |
| return (f1->index - f2->index); | |
| } | |
| WINE_HIDP_PREPARSED_DATA* ParseDescriptor(BYTE *descriptor, unsigned int length) | |
| { | |
| WINE_HIDP_PREPARSED_DATA *data = NULL; | |
| struct collection *base; | |
| struct caps caps; | |
| struct list features; | |
| struct list caps_stack; | |
| unsigned int feature_count = 0; | |
| unsigned int cidx; | |
| if (TRACE_ON(hid)) | |
| { | |
| TRACE("Descriptor[%i]: ", length); | |
| for (cidx = 0; cidx < length; cidx++) | |
| { | |
| TRACE("%x ",descriptor[cidx]); | |
| if ((cidx+1) % 80 == 0) | |
| TRACE("\n"); | |
| } | |
| TRACE("\n"); | |
| } | |
| list_init(&features); | |
| list_init(&caps_stack); | |
| base = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*base)); | |
| base->index = 1; | |
| list_init(&base->features); | |
| list_init(&base->collections); | |
| memset(&caps, 0, sizeof(caps)); | |
| cidx = 0; | |
| parse_descriptor(descriptor, 0, length, &feature_count, &cidx, base, &caps, &features, &caps_stack); | |
| debug_collection(base); | |
| if (!list_empty(&caps_stack)) | |
| { | |
| struct caps_stack *entry, *cursor; | |
| ERR("%i unpopped device caps on the stack\n", list_count(&caps_stack)); | |
| LIST_FOR_EACH_ENTRY_SAFE(entry, cursor, &caps_stack, struct caps_stack, entry) | |
| { | |
| list_remove(&entry->entry); | |
| HeapFree(GetProcessHeap(), 0, entry); | |
| } | |
| } | |
| cidx = 2; | |
| if (feature_count) | |
| { | |
| struct feature *entry; | |
| struct feature** sorted_features; | |
| struct feature** input_features; | |
| struct feature** output_features; | |
| struct feature** feature_features; | |
| unsigned int i_count, o_count, f_count; | |
| unsigned int i; | |
| i_count = o_count = f_count = 0; | |
| sorted_features = HeapAlloc(GetProcessHeap(), 0, sizeof(*sorted_features) * feature_count); | |
| input_features = HeapAlloc(GetProcessHeap(), 0, sizeof(*input_features) * feature_count); | |
| output_features = HeapAlloc(GetProcessHeap(), 0, sizeof(*output_features) * feature_count); | |
| feature_features = HeapAlloc(GetProcessHeap(), 0, sizeof(*feature_features) * feature_count); | |
| i = 0; | |
| LIST_FOR_EACH_ENTRY(entry, &features, struct feature, entry) | |
| sorted_features[i++] = entry; | |
| /* Sort features base on report if there are multiple reports */ | |
| if (sorted_features[0]->caps.ReportID != 0) | |
| qsort(sorted_features, feature_count, sizeof(struct feature*), compare_reports); | |
| for (i = 0; i < feature_count; i++) | |
| { | |
| switch (sorted_features[i]->type) | |
| { | |
| case HidP_Input: | |
| input_features[i_count] = sorted_features[i]; | |
| i_count++; | |
| break; | |
| case HidP_Output: | |
| output_features[o_count] = sorted_features[i]; | |
| o_count++; | |
| break; | |
| case HidP_Feature: | |
| feature_features[f_count] = sorted_features[i]; | |
| f_count++; | |
| break; | |
| default: | |
| ERR("Unknown type %i\n",sorted_features[i]->type); | |
| } | |
| } | |
| if (TRACE_ON(hid)) | |
| { | |
| TRACE("DUMP FEATURES:\n"); | |
| TRACE("----INPUT----\n"); | |
| for (cidx = 0; cidx < i_count; cidx++) | |
| debug_feature(input_features[cidx]); | |
| TRACE("----OUTPUT----\n"); | |
| for (cidx = 0; cidx < o_count; cidx++) | |
| debug_feature(output_features[cidx]); | |
| TRACE("----FEATURE----\n"); | |
| for (cidx = 0; cidx < f_count; cidx++) | |
| debug_feature(feature_features[cidx]); | |
| } | |
| data = build_PreparseData(sorted_features, feature_count, input_features, i_count, output_features, o_count, feature_features, f_count, base); | |
| debug_print_preparsed(data); | |
| HeapFree(GetProcessHeap(), 0, sorted_features); | |
| HeapFree(GetProcessHeap(), 0, input_features); | |
| HeapFree(GetProcessHeap(), 0, output_features); | |
| HeapFree(GetProcessHeap(), 0, feature_features); | |
| } | |
| free_collection(base); | |
| /* We do not have to free the list as free_collection does all the work */ | |
| return data; | |
| } |