894bdc6ed19fda346ffc4506dd2174cb2afebbbe
[silc.git] / apps / silcd / idlist.c
1 /*
2
3   idlist.c
4
5   Author: Pekka Riikonen <priikone@poseidon.pspt.fi>
6
7   Copyright (C) 1997 - 2001 Pekka Riikonen
8
9   This program is free software; you can redistribute it and/or modify
10   it under the terms of the GNU General Public License as published by
11   the Free Software Foundation; either version 2 of the License, or
12   (at your option) any later version.
13   
14   This program is distributed in the hope that it will be useful,
15   but WITHOUT ANY WARRANTY; without even the implied warranty of
16   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   GNU General Public License for more details.
18
19 */
20 /* $Id$ */
21
22 #include "serverincludes.h"
23 #include "idlist.h"
24
25 /******************************************************************************
26
27                              Common functions
28
29 ******************************************************************************/
30
31 /* This function is used to add keys and stuff to common ID entry data
32    structure. */
33
34 void silc_idlist_add_data(void *entry, SilcIDListData idata)
35 {
36   SilcIDListData data = (SilcIDListData)entry;
37   data->send_key = idata->send_key;
38   data->receive_key = idata->receive_key;
39   data->rekey = idata->rekey;
40   data->hash = idata->hash;
41   data->hmac_send = idata->hmac_send;
42   data->hmac_receive = idata->hmac_receive;
43   data->public_key = idata->public_key;
44   data->last_receive = idata->last_receive;
45   data->last_sent = idata->last_sent;
46   data->status = idata->status;
47 }
48
49 /* Free's all data in the common ID entry data structure. */
50
51 void silc_idlist_del_data(void *entry)
52 {
53   SilcIDListData idata = (SilcIDListData)entry;
54   if (idata->send_key)
55     silc_cipher_free(idata->send_key);
56   if (idata->receive_key)
57     silc_cipher_free(idata->receive_key);
58   if (idata->rekey) {
59     if (idata->rekey->send_enc_key) {
60       memset(idata->rekey->send_enc_key, 0, idata->rekey->enc_key_len);
61       silc_free(idata->rekey->send_enc_key);
62     }
63     silc_free(idata->rekey);
64   }
65   if (idata->hmac_send)         /* Same as idata->hmac_receive */
66     silc_hmac_free(idata->hmac_send);
67   if (idata->public_key)
68     silc_pkcs_public_key_free(idata->public_key);
69 }
70
71 /* Purges ID cache */
72
73 SILC_TASK_CALLBACK_GLOBAL(silc_idlist_purge)
74 {
75   SilcIDListPurge i = (SilcIDListPurge)context;
76
77   SILC_LOG_DEBUG(("Start"));
78
79   silc_idcache_purge(i->cache);
80   silc_schedule_task_add(i->schedule, 0, 
81                          silc_idlist_purge,
82                          (void *)i, 600, 0,
83                          SILC_TASK_TIMEOUT, SILC_TASK_PRI_LOW);
84 }
85
86 /******************************************************************************
87
88                           Server entry functions
89
90 ******************************************************************************/
91
92 /* Add new server entry. This adds the new server entry to ID cache and
93    returns the allocated entry object or NULL on error. This is called
94    when new server connects to us. We also add ourselves to cache with
95    this function. */
96
97 SilcServerEntry 
98 silc_idlist_add_server(SilcIDList id_list, 
99                        char *server_name, int server_type,
100                        SilcServerID *id, SilcServerEntry router,
101                        void *connection)
102 {
103   SilcServerEntry server;
104
105   SILC_LOG_DEBUG(("Adding new server entry"));
106
107   server = silc_calloc(1, sizeof(*server));
108   server->server_name = server_name;
109   server->server_type = server_type;
110   server->id = id;
111   server->router = router;
112   server->connection = connection;
113
114   if (!silc_idcache_add(id_list->servers, server->server_name, 
115                         (void *)server->id, (void *)server, FALSE)) {
116     silc_free(server);
117     return NULL;
118   }
119
120   return server;
121 }
122
123 /* Finds server by Server ID */
124
125 SilcServerEntry
126 silc_idlist_find_server_by_id(SilcIDList id_list, SilcServerID *id,
127                               bool registered, SilcIDCacheEntry *ret_entry)
128 {
129   SilcIDCacheEntry id_cache = NULL;
130   SilcServerEntry server;
131
132   if (!id)
133     return NULL;
134
135   SILC_LOG_DEBUG(("Server ID (%s)",
136                   silc_id_render(id, SILC_ID_SERVER)));
137
138   if (!silc_idcache_find_by_id_one(id_list->servers, (void *)id, 
139                                    &id_cache))
140     return NULL;
141
142   server = (SilcServerEntry)id_cache->context;
143
144   if (ret_entry)
145     *ret_entry = id_cache;
146
147   if (server && registered && 
148       !(server->data.status & SILC_IDLIST_STATUS_REGISTERED))
149     return NULL;
150
151   SILC_LOG_DEBUG(("Found"));
152
153   return server;
154 }
155
156 /* Find server by name */
157
158 SilcServerEntry
159 silc_idlist_find_server_by_name(SilcIDList id_list, char *name,
160                                 bool registered, SilcIDCacheEntry *ret_entry)
161 {
162   SilcIDCacheEntry id_cache = NULL;
163   SilcServerEntry server;
164
165   SILC_LOG_DEBUG(("Server by name `%s'", name));
166
167   if (!silc_idcache_find_by_name_one(id_list->servers, name, &id_cache))
168     return NULL;
169
170   server = (SilcServerEntry)id_cache->context;
171   
172   if (ret_entry)
173     *ret_entry = id_cache;
174
175   if (server && registered &&
176       !(server->data.status & SILC_IDLIST_STATUS_REGISTERED))
177     return NULL;
178
179   SILC_LOG_DEBUG(("Found"));
180
181   return server;
182 }
183
184 /* Find server by connection parameters, hostname and port */
185
186 SilcServerEntry
187 silc_idlist_find_server_by_conn(SilcIDList id_list, char *hostname,
188                                 int port, bool registered,
189                                 SilcIDCacheEntry *ret_entry)
190 {
191   SilcIDCacheList list = NULL;
192   SilcIDCacheEntry id_cache = NULL;
193   SilcServerEntry server = NULL;
194   SilcSocketConnection sock;
195  
196   SILC_LOG_DEBUG(("Server by hostname %s and port %d", hostname, port));
197
198   if (!silc_idcache_get_all(id_list->servers, &list))
199     return NULL;
200
201   if (!silc_idcache_list_first(list, &id_cache)) {
202     silc_idcache_list_free(list);
203     return NULL;
204   }
205
206   while (id_cache) {
207     server = (SilcServerEntry)id_cache->context;
208     sock = (SilcSocketConnection)server->connection;
209     
210     if (sock && ((sock->hostname && !strcasecmp(sock->hostname, hostname)) ||
211                  (sock->ip && !strcasecmp(sock->ip, hostname)))
212         && sock->port == port)
213       break;
214
215     id_cache = NULL;
216     server = NULL;
217
218     if (!silc_idcache_list_next(list, &id_cache))
219       break;
220   }
221   
222   silc_idcache_list_free(list);
223
224   if (ret_entry)
225     *ret_entry = id_cache;
226
227   if (server && registered &&
228       !(server->data.status & SILC_IDLIST_STATUS_REGISTERED))
229     return NULL;
230
231   SILC_LOG_DEBUG(("Found"));
232
233   return server;
234 }
235
236 /* Replaces old Server ID with new one */ 
237
238 SilcServerEntry
239 silc_idlist_replace_server_id(SilcIDList id_list, SilcServerID *old_id,
240                               SilcServerID *new_id)
241 {
242   SilcIDCacheEntry id_cache = NULL;
243   SilcServerEntry server;
244
245   if (!old_id || !new_id)
246     return NULL;
247
248   SILC_LOG_DEBUG(("Replacing Server ID"));
249
250   if (!silc_idcache_find_by_id_one(id_list->servers, (void *)old_id, 
251                                    &id_cache))
252     return NULL;
253
254   server = (SilcServerEntry)id_cache->context;
255
256   /* Remove the old entry and add a new one */
257
258   silc_idcache_del_by_id(id_list->servers, (void *)server->id);
259
260   silc_free(server->id);
261   server->id = new_id;
262
263   silc_idcache_add(id_list->servers, server->server_name, server->id, 
264                    server, FALSE);
265
266   SILC_LOG_DEBUG(("Found"));
267
268   return server;
269 }
270
271 /* Removes and free's server entry from ID list */
272
273 int silc_idlist_del_server(SilcIDList id_list, SilcServerEntry entry)
274 {
275   SILC_LOG_DEBUG(("Start"));
276
277   if (entry) {
278     /* Remove from cache */
279     if (entry->id)
280       if (!silc_idcache_del_by_id(id_list->servers, (void *)entry->id))
281         return FALSE;
282
283     /* Free data */
284     silc_free(entry->server_name);
285     silc_free(entry->id);
286
287     memset(entry, 'F', sizeof(*entry));
288     silc_free(entry);
289     return TRUE;
290   }
291
292   return FALSE;
293 }
294
295 /******************************************************************************
296
297                           Client entry functions
298
299 ******************************************************************************/
300
301 /* Add new client entry. This adds the client entry to ID cache system
302    and returns the allocated client entry or NULL on error.  This is
303    called when new client connection is accepted to the server. If The
304    `router' is provided then the all server routines assume that the client
305    is not directly connected local client but it has router set and is
306    remote.  If this is the case then `connection' must be NULL.  If, on the
307    other hand, the `connection' is provided then the client is assumed
308    to be directly connected local client and `router' must be NULL. */
309
310 SilcClientEntry
311 silc_idlist_add_client(SilcIDList id_list, char *nickname, char *username, 
312                        char *userinfo, SilcClientID *id, 
313                        SilcServerEntry router, void *connection)
314 {
315   SilcClientEntry client;
316
317   SILC_LOG_DEBUG(("Adding new client entry"));
318
319   client = silc_calloc(1, sizeof(*client));
320   client->nickname = nickname;
321   client->username = username;
322   client->userinfo = userinfo;
323   client->id = id;
324   client->router = router;
325   client->connection = connection;
326   client->channels = silc_hash_table_alloc(3, silc_hash_ptr, NULL,
327                                            NULL, NULL, NULL, NULL, TRUE);
328
329   if (!silc_idcache_add(id_list->clients, nickname, (void *)client->id, 
330                         (void *)client, FALSE)) {
331     silc_hash_table_free(client->channels);
332     silc_free(client);
333     return NULL;
334   }
335
336   return client;
337 }
338
339 /* Free client entry. This free's everything and removes the entry
340    from ID cache. Call silc_idlist_del_data before calling this one. */
341
342 int silc_idlist_del_client(SilcIDList id_list, SilcClientEntry entry)
343 {
344   SILC_LOG_DEBUG(("Start"));
345
346   if (entry) {
347     /* Remove from cache */
348     if (entry->id)
349       if (!silc_idcache_del_by_context(id_list->clients, entry))
350         return FALSE;
351
352     /* Free data */
353     silc_free(entry->nickname);
354     silc_free(entry->username);
355     silc_free(entry->userinfo);
356     silc_free(entry->id);
357
358     memset(entry, 'F', sizeof(*entry));
359     silc_free(entry);
360
361     return TRUE;
362   }
363
364   return FALSE;
365 }
366
367 /* Returns all clients matching requested nickname. Number of clients is
368    returned to `clients_count'. Caller must free the returned table. */
369
370 int silc_idlist_get_clients_by_nickname(SilcIDList id_list, char *nickname,
371                                         char *server, 
372                                         SilcClientEntry **clients,
373                                         uint32 *clients_count)
374 {
375   SilcIDCacheList list = NULL;
376   SilcIDCacheEntry id_cache = NULL;
377
378   SILC_LOG_DEBUG(("Start"));
379
380   if (!silc_idcache_find_by_name(id_list->clients, nickname, &list))
381     return FALSE;
382
383   *clients = silc_realloc(*clients, 
384                           (silc_idcache_list_count(list) + *clients_count) * 
385                           sizeof(**clients));
386
387   silc_idcache_list_first(list, &id_cache);
388   (*clients)[(*clients_count)++] = (SilcClientEntry)id_cache->context;
389
390   while (silc_idcache_list_next(list, &id_cache))
391     (*clients)[(*clients_count)++] = (SilcClientEntry)id_cache->context;
392   
393   silc_idcache_list_free(list);
394   
395   SILC_LOG_DEBUG(("Found total %d clients", *clients_count));
396
397   return TRUE;
398 }
399
400 /* Returns all clients matching requested nickname hash. Number of clients
401    is returned to `clients_count'. Caller must free the returned table. */
402
403 int silc_idlist_get_clients_by_hash(SilcIDList id_list, char *nickname,
404                                     SilcHash md5hash,
405                                     SilcClientEntry **clients,
406                                     uint32 *clients_count)
407 {
408   SilcIDCacheList list = NULL;
409   SilcIDCacheEntry id_cache = NULL;
410   unsigned char hash[32];
411   SilcClientID client_id;
412
413   SILC_LOG_DEBUG(("Start"));
414
415   silc_hash_make(md5hash, nickname, strlen(nickname), hash);
416
417   /* As the Client ID is hashed in the ID cache by hashing only the hash
418      from the Client ID, we can do a lookup with only the hash not the
419      other parts of the ID and get all the clients with that hash, ie.
420      with that nickname, as the hash is from the nickname. */
421   memset(&client_id, 0, sizeof(client_id));
422   memcpy(&client_id.hash, hash, sizeof(client_id.hash));
423   if (!silc_idcache_find_by_id(id_list->clients, &client_id, &list))
424     return FALSE;
425
426   *clients = silc_realloc(*clients, 
427                           (silc_idcache_list_count(list) + *clients_count) * 
428                           sizeof(**clients));
429
430   silc_idcache_list_first(list, &id_cache);
431   (*clients)[(*clients_count)++] = (SilcClientEntry)id_cache->context;
432
433   while (silc_idcache_list_next(list, &id_cache))
434     (*clients)[(*clients_count)++] = (SilcClientEntry)id_cache->context;
435   
436   silc_idcache_list_free(list);
437   
438   SILC_LOG_DEBUG(("Found total %d clients", *clients_count));
439
440   return TRUE;
441 }
442
443 /* Finds client by Client ID */
444
445 SilcClientEntry
446 silc_idlist_find_client_by_id(SilcIDList id_list, SilcClientID *id,
447                               bool registered, SilcIDCacheEntry *ret_entry)
448 {
449   SilcIDCacheEntry id_cache = NULL;
450   SilcClientEntry client;
451
452   if (!id)
453     return NULL;
454
455   SILC_LOG_DEBUG(("Client ID (%s)", 
456                   silc_id_render(id, SILC_ID_CLIENT)));
457
458   /* Do extended search since the normal ID comparison function for
459      Client ID's compares only the hash from the Client ID and not the
460      entire ID. The silc_hash_client_id_compare compares the entire
461      Client ID as we want to find one specific Client ID. */
462   if (!silc_idcache_find_by_id_one_ext(id_list->clients, (void *)id, 
463                                        NULL, NULL, 
464                                        silc_hash_client_id_compare, NULL,
465                                        &id_cache))
466     return NULL;
467
468   client = (SilcClientEntry)id_cache->context;
469
470   if (ret_entry)
471     *ret_entry = id_cache;
472
473   if (client && registered &&
474       !(client->data.status & SILC_IDLIST_STATUS_REGISTERED))
475     return NULL;
476
477   SILC_LOG_DEBUG(("Found"));
478
479   return client;
480 }
481
482 /* Replaces old Client ID with new one */
483
484 SilcClientEntry
485 silc_idlist_replace_client_id(SilcIDList id_list, SilcClientID *old_id,
486                               SilcClientID *new_id)
487 {
488   SilcIDCacheEntry id_cache = NULL;
489   SilcClientEntry client;
490
491   if (!old_id || !new_id)
492     return NULL;
493
494   SILC_LOG_DEBUG(("Replacing Client ID"));
495
496   /* Do extended search since the normal ID comparison function for
497      Client ID's compares only the hash from the Client ID and not the
498      entire ID. The silc_hash_client_id_compare compares the entire
499      Client ID as we want to find one specific Client ID. */
500   if (!silc_idcache_find_by_id_one_ext(id_list->clients, (void *)old_id, 
501                                        NULL, NULL, 
502                                        silc_hash_client_id_compare, NULL,
503                                        &id_cache))
504     return NULL;
505
506   client = (SilcClientEntry)id_cache->context;
507
508   /* Remove the old entry and add a new one */
509
510   silc_idcache_del_by_context(id_list->clients, client);
511
512   silc_free(client->id);
513   client->id = new_id;
514
515   silc_idcache_add(id_list->clients, NULL, client->id, client, FALSE);
516
517   SILC_LOG_DEBUG(("Replaced"));
518
519   return client;
520 }
521
522 /* Client cache entry destructor that is called when the cache is purged. */
523
524 void silc_idlist_client_destructor(SilcIDCache cache,
525                                    SilcIDCacheEntry entry)
526 {
527   SilcClientEntry client;
528
529   SILC_LOG_DEBUG(("Start"));
530
531   client = (SilcClientEntry)entry->context;
532   if (client) {
533     if (client->nickname)
534       silc_free(client->nickname);
535     if (client->username)
536       silc_free(client->username);
537     if (client->userinfo)
538       silc_free(client->userinfo);
539     if (client->id)
540       silc_free(client->id);
541
542     memset(client, 'F', sizeof(*client));
543     silc_free(client);
544   }
545 }
546
547 /******************************************************************************
548
549                           Channel entry functions
550
551 ******************************************************************************/
552
553 /* Add new channel entry. This add the new channel entry to the ID cache
554    system and returns the allocated entry or NULL on error. */
555
556 SilcChannelEntry
557 silc_idlist_add_channel(SilcIDList id_list, char *channel_name, int mode,
558                         SilcChannelID *id, SilcServerEntry router,
559                         SilcCipher channel_key, SilcHmac hmac)
560 {
561   SilcChannelEntry channel;
562
563   SILC_LOG_DEBUG(("Adding new channel entry"));
564
565   channel = silc_calloc(1, sizeof(*channel));
566   channel->channel_name = channel_name;
567   channel->mode = mode;
568   channel->id = id;
569   channel->router = router;
570   channel->channel_key = channel_key;
571   channel->hmac = hmac;
572   if (!channel->hmac)
573     if (!silc_hmac_alloc(SILC_DEFAULT_HMAC, NULL, &channel->hmac)) {
574       silc_free(channel);
575       return NULL;
576     }
577
578   channel->user_list = silc_hash_table_alloc(3, silc_hash_ptr, NULL, NULL,
579                                              NULL, NULL, NULL, TRUE);
580
581   if (!silc_idcache_add(id_list->channels, channel->channel_name, 
582                         (void *)channel->id, (void *)channel, FALSE)) {
583     silc_hmac_free(channel->hmac);
584     silc_hash_table_free(channel->user_list);
585     silc_free(channel);
586     return NULL;
587   }
588
589   return channel;
590 }
591
592 /* Foreach callbcak to free all users from the channel when deleting a
593    channel entry. */
594
595 static void silc_idlist_del_channel_foreach(void *key, void *context,
596                                             void *user_context)
597 {
598   SilcChannelClientEntry chl = (SilcChannelClientEntry)context;
599
600   /* Remove the context from the client's channel hash table as that
601      table and channel's user_list hash table share this same context. */
602   silc_hash_table_del(chl->client->channels, chl->channel);
603   silc_free(chl);
604 }
605
606 /* Free channel entry.  This free's everything. */
607
608 int silc_idlist_del_channel(SilcIDList id_list, SilcChannelEntry entry)
609 {
610   SILC_LOG_DEBUG(("Start"));
611
612   if (entry) {
613     /* Remove from cache */
614     if (entry->id)
615       if (!silc_idcache_del_by_id(id_list->channels, (void *)entry->id))
616         return FALSE;
617
618     /* Free data */
619     silc_free(entry->channel_name);
620     silc_free(entry->id);
621     silc_free(entry->topic);
622     if (entry->channel_key)
623       silc_cipher_free(entry->channel_key);
624     if (entry->key) {
625       memset(entry->key, 0, entry->key_len / 8);
626       silc_free(entry->key);
627     }
628     silc_free(entry->cipher);
629     silc_free(entry->hmac_name);
630     silc_free(entry->rekey);
631
632     /* Free all client entrys from the users list. The silc_hash_table_free
633        will free all the entries so they are not freed at the foreach 
634        callback. */
635     silc_hash_table_foreach(entry->user_list, silc_idlist_del_channel_foreach,
636                             NULL);
637     silc_hash_table_free(entry->user_list);
638
639     memset(entry, 'F', sizeof(*entry));
640     silc_free(entry);
641     return TRUE;
642   }
643
644   return FALSE;
645 }
646
647 /* Finds channel by channel name. Channel names are unique and they
648    are not case-sensitive. */
649
650 SilcChannelEntry
651 silc_idlist_find_channel_by_name(SilcIDList id_list, char *name,
652                                  SilcIDCacheEntry *ret_entry)
653 {
654   SilcIDCacheEntry id_cache = NULL;
655
656   SILC_LOG_DEBUG(("Channel by name"));
657
658   if (!silc_idcache_find_by_name_one(id_list->channels, name, &id_cache))
659     return NULL;
660
661   if (ret_entry)
662     *ret_entry = id_cache;
663
664   SILC_LOG_DEBUG(("Found"));
665
666   return id_cache->context;
667 }
668
669 /* Finds channel by Channel ID. */
670
671 SilcChannelEntry
672 silc_idlist_find_channel_by_id(SilcIDList id_list, SilcChannelID *id,
673                                SilcIDCacheEntry *ret_entry)
674 {
675   SilcIDCacheEntry id_cache = NULL;
676   SilcChannelEntry channel;
677
678   if (!id)
679     return NULL;
680
681   SILC_LOG_DEBUG(("Channel ID (%s)",
682                   silc_id_render(id, SILC_ID_CHANNEL)));
683
684   if (!silc_idcache_find_by_id_one(id_list->channels, (void *)id, &id_cache))
685     return NULL;
686
687   channel = (SilcChannelEntry)id_cache->context;
688
689   if (ret_entry)
690     *ret_entry = id_cache;
691
692   SILC_LOG_DEBUG(("Found"));
693
694   return channel;
695 }
696
697 /* Replaces old Channel ID with new one. This is done when router forces
698    normal server to change Channel ID. */
699
700 SilcChannelEntry
701 silc_idlist_replace_channel_id(SilcIDList id_list, SilcChannelID *old_id,
702                                SilcChannelID *new_id)
703 {
704   SilcIDCacheEntry id_cache = NULL;
705   SilcChannelEntry channel;
706
707   if (!old_id || !new_id)
708     return NULL;
709
710   SILC_LOG_DEBUG(("Replacing Channel ID"));
711
712   if (!silc_idcache_find_by_id_one(id_list->channels, (void *)old_id, 
713                                    &id_cache))
714     return NULL;
715
716   channel = (SilcChannelEntry)id_cache->context;
717
718   /* Remove the old entry and add a new one */
719
720   silc_idcache_del_by_id(id_list->channels, (void *)channel->id);
721
722   silc_free(channel->id);
723   channel->id = new_id;
724
725   silc_idcache_add(id_list->channels, channel->channel_name, channel->id, 
726                    channel, FALSE);
727
728   SILC_LOG_DEBUG(("Replaced"));
729
730   return channel;
731 }
732
733 /* Returns channels from the ID list. If the `channel_id' is NULL then
734    all channels are returned. */
735
736 SilcChannelEntry *
737 silc_idlist_get_channels(SilcIDList id_list, SilcChannelID *channel_id,
738                          uint32 *channels_count)
739 {
740   SilcIDCacheList list = NULL;
741   SilcIDCacheEntry id_cache = NULL;
742   SilcChannelEntry *channels = NULL;
743   int i = 0;
744
745   SILC_LOG_DEBUG(("Start"));
746
747   if (!channel_id) {
748     if (!silc_idcache_get_all(id_list->channels, &list))
749       return NULL;
750
751     channels = silc_calloc(silc_idcache_list_count(list), sizeof(*channels));
752     
753     i = 0;
754     silc_idcache_list_first(list, &id_cache);
755     channels[i++] = (SilcChannelEntry)id_cache->context;
756     
757     while (silc_idcache_list_next(list, &id_cache))
758       channels[i++] = (SilcChannelEntry)id_cache->context;
759     
760     silc_idcache_list_free(list);
761   } else {
762     if (!silc_idcache_find_by_id_one(id_list->channels, channel_id, &id_cache))
763       return NULL;
764
765     i = 1;
766     channels = silc_calloc(1, sizeof(*channels));
767     channels[0] = (SilcChannelEntry)id_cache->context;
768   }
769
770   if (channels_count)
771     *channels_count = i;
772
773   return channels;
774 }