1/******************************************************************************
2 *
3 *  Copyright (C) 2009-2012 Broadcom Corporation
4 *
5 *  Licensed under the Apache License, Version 2.0 (the "License");
6 *  you may not use this file except in compliance with the License.
7 *  You may obtain a copy of the License at:
8 *
9 *  http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 *
17 ******************************************************************************/
18
19#include <stdio.h>
20#include <errno.h>
21#include <fcntl.h>
22#include <sys/types.h>
23#include <sys/stat.h>
24#include <sys/statfs.h>
25#include <sys/vfs.h>
26#include <unistd.h>
27#include <dirent.h>
28#include <limits.h>
29#include <sys/file.h>
30#include <sys/mman.h>
31
32#include "btif_config.h"
33#include "btif_config_util.h"
34#ifndef ANDROID_NDK
35#define ANDROID_NDK
36#endif
37#include "tinyxml2.h"
38#ifndef FALSE
39#define TRUE 1
40#define FALSE 0
41#endif
42#define LOG_TAG "btif_config_util"
43extern "C" {
44#include "btif_sock_util.h"
45}
46#include <stdlib.h>
47#include <cutils/log.h>
48#define info(fmt, ...)  ALOGI ("%s(L%d): " fmt,__FUNCTION__, __LINE__,  ## __VA_ARGS__)
49#define debug(fmt, ...) ALOGD ("%s(L%d): " fmt,__FUNCTION__, __LINE__,  ## __VA_ARGS__)
50#define warn(fmt, ...) ALOGW ("## WARNING : %s(L%d): " fmt "##",__FUNCTION__, __LINE__, ## __VA_ARGS__)
51#define error(fmt, ...) ALOGE ("## ERROR : %s(L%d): " fmt "##",__FUNCTION__, __LINE__, ## __VA_ARGS__)
52#define asrt(s) if(!(s)) ALOGE ("## %s assert %s failed at line:%d ##",__FUNCTION__, #s, __LINE__)
53
54#define BLUEDROID_ROOT "Bluedroid"
55#define BLUEDROID_NAME_TAG "Tag"
56#define BLUEDROID_VALUE_TYPE "Type"
57#define BLUEDROID_TAG_REMOTE_DEVICE "Remote Devices"
58
59using namespace tinyxml2;
60struct enum_user_data
61{
62    const char* sn; //current section name
63    const char* kn; //current key name
64    const char* vn; //current value name
65    int si, ki, vi;
66    XMLDocument* xml;
67    XMLElement* se;
68    XMLElement* ke;
69    XMLElement* ve;
70};
71
72
73static int type_str2int(const char* type);
74static const char* type_int2str(int type);
75static inline void create_ele_name(int index, char* element, int len);
76static inline int validate_ele_name(const char* key);
77static int parse_sections(const char* section_name, const XMLElement* section);
78static void enum_config(void* user_data, const char* section, const char* key, const char* name,
79                                          const char*  value, int bytes, int type);
80static inline void bytes2hex(const char* data, int bytes, char* str)
81{
82    static const char* hex_table = "0123456789abcdef";
83    for(int i = 0; i < bytes; i++)
84    {
85        *str = hex_table[(data[i] >> 4) & 0xf];
86        ++str;
87        *str = hex_table[data[i] & 0xf];
88        ++str;
89    }
90    *str = 0;
91}
92static inline int hex2byte(char hex)
93{
94    if('0' <= hex && hex <= '9')
95        return hex - '0';
96    if('a' <= hex && hex <= 'z')
97        return hex - 'a' + 0xa;
98    if('A' <= hex && hex <= 'Z')
99        return hex - 'A' + 0xa;
100    return -1;
101}
102static inline int trim_bin_str_value(const char** str)
103{
104    while(**str == ' ' || **str == '\r' || **str == '\t' || **str == '\n')
105        (*str)++;
106    int len = 0;
107    const char* s = *str;
108    while(*s && *s != ' ' && *s != '\r' && *s != '\t' && *s != '\n')
109    {
110        len++;
111        s++;
112    }
113    return len;
114}
115static inline bool hex2bytes(const char* str, int len, char* data)
116{
117    if(len % 2)
118    {
119        error("cannot convert odd len hex str: %s, len:%d to binary", str, len);
120        return false;
121    }
122    for(int i = 0; i < len; i+= 2)
123    {
124        int d = hex2byte(str[i]);
125        if(d < 0)
126        {
127            error("cannot convert hex: %s, len:%d to binary", str, len);
128            return false;
129        }
130        *data = (char)(d << 4);
131        d = hex2byte(str[i+1]);
132        if(d < 0)
133        {
134            error("cannot convert hex: %s, len:%d to binary", str, len);
135            return false;
136        }
137        *data++ |= (char)d;
138    }
139    return true;
140}
141static inline void reverse_bin(char *bin, int size)
142{
143    for(int i = 0; i < size /2; i++)
144    {
145        int b = bin[i];
146        bin[i] = bin[size - i - 1];
147        bin[size -i  - 1] = b;
148    }
149}
150////////////////////////////////////////////////////////////////////////////////////////////////////////
151int btif_config_save_file(const char* file_name)
152{
153    debug("in file name:%s", file_name);
154    XMLDocument xml;
155    XMLElement* root = xml.NewElement(BLUEDROID_ROOT);
156    xml.InsertFirstChild(root);
157    int ret = FALSE;
158    enum_user_data data;
159    memset(&data, 0, sizeof(data));
160    data.xml = &xml;
161    if(btif_config_enum(enum_config, &data))
162        ret = xml.SaveFile(file_name) == XML_SUCCESS;
163    return ret;
164}
165int btif_config_load_file(const char* file_name)
166{
167    //if(access(file_name, 0) != 0)
168    //    return XML_ERROR_FILE_NOT_FOUND;
169    XMLDocument xml;
170    int err = xml.LoadFile(file_name);
171    const XMLElement* root = xml.RootElement();
172    int ret = FALSE;
173    if(err == XML_SUCCESS && root && strcmp(root->Name(), BLUEDROID_ROOT) == 0)
174    {
175        const XMLElement* section;
176        for(section = root->FirstChildElement(); section; section = section->NextSiblingElement())
177        {
178            //debug("section tag:%s", section->Name());
179            if(validate_ele_name(section->Name()))
180            {
181                const char* section_name = section->Attribute(BLUEDROID_NAME_TAG);
182                if(section_name && *section_name)
183                    if(parse_sections(section_name, section))
184                        ret = TRUE;
185            }
186        }
187    }
188    return ret;
189}
190//////////////////////////////////////////////////////////////////////////////////////////////////////////
191static int parse_sections(const char* section_name, const XMLElement* section)
192{
193    const XMLElement* key;
194    //debug("in");
195    for(key = section->FirstChildElement(); key; key = key->NextSiblingElement())
196    {
197        //debug("key tag:%s", key->Name());
198        if(validate_ele_name(key->Name()))
199        {
200            const char* key_name = key->Attribute(BLUEDROID_NAME_TAG);
201            //debug("key name:%s", key_name);
202            if(key_name && *key_name)
203            {
204                const XMLElement* value;
205                for(value = key->FirstChildElement(); value; value = value->NextSiblingElement())
206                {
207                    const char* value_name = value->Attribute(BLUEDROID_NAME_TAG);
208                    const char* value_type = value->Attribute(BLUEDROID_VALUE_TYPE);
209                    //debug("value ele name:%s, section name:%s, key name:%s, value name:%s, value type:%s",
210                    //        value->Name(), section_name, key_name, value_name, value_type);
211                    int type = type_str2int(value_type);
212                    if(value_name && *value_name && type != BTIF_CFG_TYPE_INVALID)
213                    {
214                        const char* value_str = value->GetText() ? value->GetText() : "";
215                        //debug("value_name:%s, value_str:%s, value_type:%s, type:%x",
216                        //       value_name, value_str, value_type, type);
217                        if(type & BTIF_CFG_TYPE_STR)
218                            btif_config_set_str(section_name, key_name, value_name, value_str);
219                        else if(type & BTIF_CFG_TYPE_INT)
220                        {
221                            if(*value_str)
222                            {
223                                int v = atoi(value_str);
224                                btif_config_set_int(section_name, key_name, value_name, v);
225                            }
226                        }
227                        else if(type & BTIF_CFG_TYPE_BIN)
228                        {
229                            int len = trim_bin_str_value(&value_str);
230                            if(len > 0 && len % 2 == 0)
231                            {
232                                char *bin = (char*)alloca(len / 2);
233                                if(hex2bytes(value_str, len, bin))
234                                    btif_config_set(section_name, key_name, value_name, bin, len/2, BTIF_CFG_TYPE_BIN);
235                            }
236                        }
237                        else error("Unsupported value:%s, type:%s not loaded", value_name, value_type);
238                    }
239                }
240            }
241        }
242    }
243    //debug("out");
244    return TRUE;
245}
246static inline XMLElement* add_ele(XMLDocument* xml, XMLElement* p, int index,
247                                  const char* name_tag, const char* value_type = NULL)
248{
249    //debug("in, tag:%s", name_tag);
250    char ele_name[128] = {0};
251    create_ele_name(index, ele_name, sizeof(ele_name));
252    XMLElement* ele = xml->NewElement(ele_name);
253    //debug("ele name:%s, tag:%s, index:%d, value type:%s", ele_name, name_tag, index, value_type);
254    ele->SetAttribute(BLUEDROID_NAME_TAG, name_tag);
255    if(value_type && *value_type)
256        ele->SetAttribute(BLUEDROID_VALUE_TYPE, value_type);
257    p->InsertEndChild(ele);
258    //debug("out, tag:%s", name_tag);
259    return ele;
260}
261static void enum_config(void* user_data, const char* section_name, const char* key_name, const char* value_name,
262                        const char*  value, int bytes, int type)
263{
264    enum_user_data& d = *(enum_user_data*)user_data;
265    //debug("in, key:%s, value:%s", key_name, value_name);
266    //debug("section name:%s, key name:%s, value name:%s, value type:%s",
267    //                      section_name, key_name, value_name, type_int2str(type));
268    if(type & BTIF_CFG_TYPE_VOLATILE)
269        return; //skip any volatile value
270    if(d.sn != section_name)
271    {
272        d.sn = section_name;
273        d.se = add_ele(d.xml, d.xml->RootElement(), ++d.si, section_name);
274        d.ki = 0;
275    }
276    if(d.kn != key_name)
277    {
278        d.kn = key_name;
279        d.ke = add_ele(d.xml, d.se, ++d.ki, key_name);
280        d.vi = 0;
281    }
282    if(d.vn != value_name)
283    {
284        if(type & BTIF_CFG_TYPE_STR)
285        {
286            d.vn = value_name;
287            d.ve = add_ele(d.xml, d.ke, ++d.vi, value_name, type_int2str(type));
288            d.ve->InsertFirstChild(d.xml->NewText(value));
289        }
290        else if(type & BTIF_CFG_TYPE_INT)
291        {
292            d.vn = value_name;
293            d.ve = add_ele(d.xml, d.ke, ++d.vi, value_name, type_int2str(type));
294            char value_str[64] = {0};
295            snprintf(value_str, sizeof(value_str), "%d", *(int*)value);
296            d.ve->InsertFirstChild(d.xml->NewText(value_str));
297        }
298        else if(type & BTIF_CFG_TYPE_BIN)
299        {
300            d.vn = value_name;
301            d.ve = add_ele(d.xml, d.ke, ++d.vi, value_name, type_int2str(type));
302            char* value_str = (char*)alloca(bytes*2 + 1);
303            bytes2hex(value, bytes, value_str);
304            d.ve->InsertFirstChild(d.xml->NewText(value_str));
305        }
306        else error("unsupported config value name:%s, type:%s not saved", d.vn, type_int2str(type));
307    }
308    //debug("out, key:%s, value:%s", key_name, value_name);
309}
310
311static int type_str2int(const char* type)
312{
313    if(strcmp(type, "int") == 0)
314        return BTIF_CFG_TYPE_INT;
315    if(strcmp(type, "binary") == 0)
316        return BTIF_CFG_TYPE_BIN;
317    if(type == 0 || *type == 0 || strcmp(type, "string") == 0)
318        return  BTIF_CFG_TYPE_STR;
319    error("unknown value type:%s", type);
320    return BTIF_CFG_TYPE_INVALID;
321}
322static const char* type_int2str(int type)
323{
324    switch(type)
325    {
326        case BTIF_CFG_TYPE_INT:
327            return "int";
328        case BTIF_CFG_TYPE_BIN:
329            return "binary";
330        case BTIF_CFG_TYPE_STR:
331            return "string";
332        default:
333            error("unknown type:%d", type);
334            break;
335    }
336    return NULL;
337}
338
339static inline void create_ele_name(int index, char* element, int len)
340{
341    snprintf(element, len, "N%d", index);
342}
343static inline int validate_ele_name(const char* key)
344{
345    //must be 'N' followed with numbers
346    if(key && *key == 'N' && *++key)
347    {
348        while(*key)
349        {
350            if(*key < '0' || *key > '9')
351                return FALSE;
352            ++key;
353        }
354        return TRUE;
355    }
356    return FALSE;
357}
358static int open_file_map(const char *pathname, const char**map, int* size)
359{
360    struct stat st;
361    st.st_size = 0;
362    int fd;
363    //debug("in");
364    if((fd = open(pathname, O_RDONLY)) >= 0)
365    {
366        //debug("fd:%d", fd);
367        if(fstat(fd, &st) == 0 && st.st_size)
368        {
369            *size = st.st_size;
370            *map = (const char*)mmap(NULL, *size, PROT_READ, MAP_SHARED, fd, 0);
371            if(*map && *map != MAP_FAILED)
372            {
373                //debug("out map:%p, size:%d", *map, *size);
374                return fd;
375            }
376        }
377        close(fd);
378    }
379    //debug("out, failed");
380    return -1;
381}
382static void close_file_map(int fd, const char* map, int size)
383{
384    munmap((void*)map, size);
385    close(fd);
386}
387static int read_file_line(const char* map, int start_pos, int size, int* line_size)
388{
389    *line_size = 0;
390    //debug("in, start pos:%d, size:%d", start_pos, size);
391    int i;
392    for(i = start_pos; i < size; i++)
393    {
394         ++*line_size;
395        if(map[i] == '\r' || map[i] == '\n')
396            break;
397    }
398    //debug("out, ret:%d, start pos:%d, size:%d, line_size:%d", i, start_pos, size, *line_size);
399    return i + 1;
400}
401static const char* find_value_line(const char* map, int size, const char *key, int* value_size)
402{
403    int key_len = strlen(key);
404    int i;
405    for(i = 0; i < size; i++)
406    {
407        if(map[i] == *key)
408        {
409            if(i + key_len + 1 > size)
410                return NULL;
411            if(memcmp(map + i, key, key_len) == 0)
412            {
413                read_file_line(map, i + key_len + 1, size, value_size);
414                if(*value_size)
415                    return map + i + key_len + 1;
416                break;
417            }
418        }
419    }
420    return NULL;
421}
422static int read_line_word(const char* line, int start_pos, int line_size, char* word, int *word_size, bool lower_case = false)
423{
424    int i;
425    //skip space
426    //debug("in, line start_pos:%d, line_size:%d", start_pos, line_size);
427    for(i = start_pos; i < line_size; i++)
428    {
429        //debug("skip space loop, line[%d]:%c", i, line[i]);
430        if(line[i] != ' ' && line[i] != '\t' && line[i] != '\r' && line[i] !='\n')
431            break;
432    }
433    *word_size = 0;
434    for(; i < line_size; i++)
435    {
436        //debug("add word loop, line[%d]:%c", i, line[i]);
437        if(line[i] != ' ' && line[i] != '\t' && line[i] != '\r' && line[i] !='\n')
438        {
439            ++*word_size;
440            if(lower_case && 'A' <= line[i] && line[i] <= 'Z')
441                *word++ = 'a' - 'A' + line[i];
442            else
443                *word++ = line[i];
444        }
445        else break;
446    }
447    *word = 0;
448    //debug("out, ret:%d, word:%s, word_size:%d, line start_pos:%d, line_size:%d",
449    //            i, word, *word_size, start_pos, line_size);
450    return i;
451}
452static int is_valid_bd_addr(const char* addr)
453{
454    int len = strlen(addr);
455    //debug("addr: %s, len:%d", addr, len);
456    return len == 17 && addr[2] == ':' && addr[5] == ':' && addr[14] == ':';
457}
458static int load_bluez_cfg_value(const char* adapter_path, const char* file_name)
459{
460    //debug("in");
461
462    const char* map = NULL;
463    int size = 0;
464    int ret = FALSE;
465    char path[256];
466    snprintf(path, sizeof(path), "%s/%s", adapter_path, file_name);
467    int fd = open_file_map(path, &map, &size);
468    //debug("in, path:%s, fd:%d, size:%d", path, fd, size);
469    if(fd < 0 || size == 0)
470    {
471        error("open_file_map fail, fd:%d, path:%s, size:%d", fd, path, size);
472        //debug("out");
473        return FALSE;
474    }
475    //get local bt device name from bluez config
476    int line_size = 0;
477    const char *value_line = find_value_line(map, size, "name", &line_size);
478    if(value_line && line_size > 0)
479    {
480        char value[line_size + 1];
481        memcpy(value, value_line, line_size);
482        value[line_size] = 0;
483        //debug("import local bt dev names:%s", value);
484        btif_config_set_str("Local", "Adapter", "Name", value);
485        ret = TRUE;
486    }
487
488    close_file_map(fd, map, size);
489    //debug("out, ret:%d", ret);
490    return ret;
491}
492
493int load_bluez_adapter_info(char* adapter_path, int size)
494{
495    struct dirent *dptr;
496    DIR *dirp;
497    int ret = FALSE;
498    if((dirp = opendir(BLUEZ_PATH)) != NULL)
499    {
500        while((dptr = readdir(dirp)) != NULL)
501        {
502            //debug("readdir: %s",dptr->d_name);
503            if(is_valid_bd_addr(dptr->d_name))
504            {
505                snprintf(adapter_path, size, "%s%s", BLUEZ_PATH, dptr->d_name);
506                btif_config_set_str("Local", "Adapter", "Address", dptr->d_name);
507                load_bluez_cfg_value(adapter_path, BLUEZ_CONFIG);
508                ret = TRUE;
509                break;
510            }
511        }
512        closedir(dirp);
513    }
514    return ret;
515}
516static inline void upcase_addr(const char* laddr, char* uaddr, int size)
517{
518    int i;
519    for(i = 0; i < size && laddr[i]; i++)
520        uaddr[i] = ('a' <= laddr[i] && laddr[i] <= 'z') ?
521                        laddr[i] - ('a' - 'A') : laddr[i];
522    uaddr[i] = 0;
523}
524static int load_bluez_dev_value(const char* adapter_path, const char* bd_addr,
525                                const char* file_name, const char* cfg_value_name, int type)
526{
527    //debug("in");
528    char addr[32];
529    upcase_addr(bd_addr, addr, sizeof(addr));
530
531    const char* map = NULL;
532    int size = 0;
533    int ret = FALSE;
534    char path[256];
535    snprintf(path, sizeof(path), "%s/%s", adapter_path, file_name);
536    int fd = open_file_map(path, &map, &size);
537    //debug("in, path:%s, addr:%s, fd:%d, size:%d", path, addr, fd, size);
538    if(fd < 0 || size == 0)
539    {
540        error("open_file_map fail, fd:%d, path:%s, size:%d", fd, path, size);
541        //debug("out");
542        return FALSE;
543    }
544    int line_size = 0;
545    const char *value_line = find_value_line(map, size, addr, &line_size);
546    if(value_line && line_size)
547    {
548        char line[line_size + 1];
549        memcpy(line, value_line, line_size);
550        line[line_size] = 0;
551        //debug("addr:%s, Names:%s", bd_addr, line);
552        if(type == BTIF_CFG_TYPE_STR)
553            btif_config_set_str("Remote", bd_addr, cfg_value_name, line);
554        else if(type == BTIF_CFG_TYPE_INT)
555        {
556            int v = strtol(line, NULL, 16);
557            //filter out unspported devices by its class
558            if(strcmp(file_name, BLUEZ_CLASSES) == 0)
559            {
560                switch((v & 0x1f00) >> 8)
561                {
562                    case 0x5: //hid device
563                        error("skip paired hid devices");
564                        close_file_map(fd, map, size);
565                        return FALSE;
566                }
567            }
568            btif_config_set_int("Remote", bd_addr, cfg_value_name, v);
569        }
570        ret = TRUE;
571    }
572    close_file_map(fd, map, size);
573    //debug("out, ret:%d", ret);
574    return ret;
575}
576static inline int bz2bd_linkkeytype(int type)
577{
578#if 1
579    return type;
580#else
581    int table[5] = {0, 0, 0, 0, 0};
582    if(0 <= type && type < (int)(sizeof(table)/sizeof(int)))
583        return table[type];
584    return 0;
585#endif
586}
587int load_bluez_linkkeys(const char* adapter_path)
588{
589    const char* map = NULL;
590    int size = 0;
591    int ret = FALSE;
592    char path[256];
593    //debug("in");
594    snprintf(path, sizeof(path), "%s/%s", adapter_path, BLUEZ_LINKKEY);
595    int fd = open_file_map(path, &map, &size);
596    if(fd < 0 || size == 0)
597    {
598        error("open_file_map fail, fd:%d, path:%s, size:%d", fd, path, size);
599        //debug("out");
600        return FALSE;
601    }
602    int pos = 0;
603    //debug("path:%s, size:%d", path, size);
604    while(pos < size)
605    {
606        int line_size = 0;
607        int next_pos = read_file_line(map, pos, size, &line_size);
608        //debug("pos:%d, next_pos:%d, size:%d, line_size:%d", pos, next_pos, size, line_size);
609        if(line_size)
610        {
611            const char* line = map + pos;
612            char addr[line_size + 1];
613            int word_pos = 0;
614            int addr_size = 0;
615            word_pos = read_line_word(line, word_pos, line_size, addr, &addr_size, true);
616            //debug("read_line_word addr:%s, addr_size:%d", addr, addr_size);
617            if(*addr)
618            {
619                char value[line_size + 1];
620                int value_size = 0;
621                //read link key
622                word_pos = read_line_word(line, word_pos, line_size, value, &value_size);
623                //debug("read_line_word linkkey:%s, size:%d", value, value_size);
624                if(*value)
625                {
626                    int linkkey_size = value_size / 2;
627                    char linkkey[linkkey_size];
628                    if(hex2bytes(value, value_size, linkkey))
629                    { //read link key type
630                        //bluez save the linkkey in reversed order
631                        reverse_bin(linkkey, linkkey_size);
632                        word_pos = read_line_word(line, word_pos,
633                                                    line_size, value, &value_size);
634                        if(*value)
635                        {
636                            if(load_bluez_dev_value(adapter_path, addr,
637                                                BLUEZ_CLASSES, "DevClass", BTIF_CFG_TYPE_INT) &&
638                               load_bluez_dev_value(adapter_path, addr,
639                                                BLUEZ_NAMES, "Name", BTIF_CFG_TYPE_STR) &&
640                               load_bluez_dev_value(adapter_path, addr,
641                                                BLUEZ_TYPES, "DevType", BTIF_CFG_TYPE_INT) &&
642                               load_bluez_dev_value(adapter_path, addr,
643                                                BLUEZ_PROFILES, "Service", BTIF_CFG_TYPE_STR))
644                            {
645                                load_bluez_dev_value(adapter_path, addr,
646                                                BLUEZ_ALIASES, "Aliase", BTIF_CFG_TYPE_STR);
647                                int key_type = bz2bd_linkkeytype(atoi(value));
648
649                                //read pin len
650                                word_pos = read_line_word(line, word_pos, line_size, value, &value_size);
651                                if(*value)
652                                {
653                                    int pin_len = atoi(value);
654                                    ret = TRUE;
655                                    btif_config_set("Remote", addr, "LinkKey", linkkey,
656                                                                    linkkey_size, BTIF_CFG_TYPE_BIN);
657                                    //dump_bin("import bluez linkkey", linkkey, linkkey_size);
658                                    btif_config_set_int("Remote", addr, "LinkKeyType", key_type);
659                                    btif_config_set_int("Remote", addr, "PinLength", pin_len);
660                                }
661                            }
662                        }
663                    }
664                }
665            }
666        }
667        //debug("pos:%d, next_pos:%d, size:%d, line_size:%d", pos, next_pos, size, line_size);
668        pos = next_pos;
669    }
670    close_file_map(fd, map, size);
671    //debug("out, ret:%d", ret);
672    return ret;
673}
674
675