]>
Commit | Line | Data |
---|---|---|
1 | /* | |
2 | * Copyright (c) 2015 Apple Inc. All rights reserved. | |
3 | * | |
4 | * @APPLE_LICENSE_HEADER_START@ | |
5 | * | |
6 | * This file contains Original Code and/or Modifications of Original Code | |
7 | * as defined in and that are subject to the Apple Public Source License | |
8 | * Version 2.0 (the 'License'). You may not use this file except in | |
9 | * compliance with the License. Please obtain a copy of the License at | |
10 | * http://www.opensource.apple.com/apsl/ and read it before using this | |
11 | * file. | |
12 | * | |
13 | * The Original Code and all software distributed under the License are | |
14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER | |
15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, | |
16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. | |
18 | * Please see the License for the specific language governing rights and | |
19 | * limitations under the License. | |
20 | * | |
21 | * @APPLE_LICENSE_HEADER_END@ | |
22 | */ | |
23 | ||
24 | #include <string.h> | |
25 | #include <stdint.h> | |
26 | #include <stdio.h> | |
27 | #include <stdlib.h> | |
28 | #include <ctype.h> | |
29 | #include <unistd.h> | |
30 | #include <stdarg.h> | |
31 | #include <syslog.h> | |
32 | #include <errno.h> | |
33 | #include <fcntl.h> | |
34 | #include <time.h> | |
35 | #include <sys/time.h> | |
36 | #include <sys/types.h> | |
37 | #include <libkern/OSAtomic.h> | |
38 | #include <crt_externs.h> | |
39 | #include <asl.h> | |
40 | #include <asl_private.h> | |
41 | #include <asl_ipc.h> | |
42 | #include <asl_core.h> | |
43 | #include <asl_client.h> | |
44 | ||
45 | #define PUBLIC_OPT_MASK 0x000000ff | |
46 | ||
47 | /* private asl_file SPI */ | |
48 | __private_extern__ ASL_STATUS asl_file_open_write_fd(int descriptor, asl_file_t **s); | |
49 | ||
50 | /* private asl SPI */ | |
51 | __private_extern__ ASL_STATUS asl_client_internal_send(asl_object_t client, asl_object_t msg, void *addr); | |
52 | ||
53 | #pragma mark - | |
54 | #pragma mark asl_client_t | |
55 | ||
56 | static void | |
57 | _asl_client_free_internal(asl_client_t *client) | |
58 | { | |
59 | uint32_t i; | |
60 | ||
61 | if (client == NULL) return; | |
62 | ||
63 | if (client->kvdict != NULL) asl_msg_release(client->kvdict); | |
64 | client->kvdict = NULL; | |
65 | ||
66 | if (client->aslfile != NULL) asl_file_close(client->aslfile); | |
67 | client->aslfile = NULL; | |
68 | ||
69 | for (i = 0; i < client->out_count; i++) | |
70 | { | |
71 | free(client->out_list[i].mfmt); | |
72 | free(client->out_list[i].tfmt); | |
73 | } | |
74 | ||
75 | free(client->out_list); | |
76 | client->out_list = NULL; | |
77 | ||
78 | free(client); | |
79 | } | |
80 | ||
81 | asl_client_t * | |
82 | asl_client_open(const char *ident, const char *facility, uint32_t opts) | |
83 | { | |
84 | asl_client_t *client = (asl_client_t *)calloc(1, sizeof(asl_client_t)); | |
85 | if (client == NULL) | |
86 | { | |
87 | errno = ENOMEM; | |
88 | return NULL; | |
89 | } | |
90 | ||
91 | client->asl_type = ASL_TYPE_CLIENT; | |
92 | client->refcount = 1; | |
93 | ||
94 | client->kvdict = asl_msg_new(ASL_TYPE_MSG); | |
95 | if (client->kvdict == NULL) | |
96 | { | |
97 | asl_client_release(client); | |
98 | errno = ENOMEM; | |
99 | return NULL; | |
100 | } | |
101 | ||
102 | client->options = opts & PUBLIC_OPT_MASK; | |
103 | ||
104 | client->pid = getpid(); | |
105 | client->uid = getuid(); | |
106 | client->gid = getgid(); | |
107 | ||
108 | if (ident != NULL) | |
109 | { | |
110 | asl_msg_set_key_val(client->kvdict, ASL_KEY_SENDER, ident); | |
111 | } | |
112 | else | |
113 | { | |
114 | char *name = *(*_NSGetArgv()); | |
115 | if (name != NULL) | |
116 | { | |
117 | char *x = strrchr(name, '/'); | |
118 | if (x != NULL) x++; | |
119 | else x = name; | |
120 | asl_msg_set_key_val(client->kvdict, ASL_KEY_SENDER, x); | |
121 | } | |
122 | } | |
123 | ||
124 | if (facility != NULL) | |
125 | { | |
126 | asl_msg_set_key_val(client->kvdict, ASL_KEY_FACILITY, facility); | |
127 | } | |
128 | else if (client->uid == 0) | |
129 | { | |
130 | asl_msg_set_key_val(client->kvdict, ASL_KEY_FACILITY, asl_syslog_faciliy_num_to_name(LOG_DAEMON)); | |
131 | } | |
132 | else | |
133 | { | |
134 | asl_msg_set_key_val(client->kvdict, ASL_KEY_FACILITY, asl_syslog_faciliy_num_to_name(LOG_USER)); | |
135 | } | |
136 | ||
137 | client->filter = ASL_FILTER_MASK_UPTO(ASL_LEVEL_NOTICE); | |
138 | ||
139 | client->filter |= EVAL_ACTIVE; | |
140 | if (!(opts & ASL_OPT_SHIM_NO_ASL)) client->filter |= EVAL_SEND_ASL; | |
141 | if (!(opts & ASL_OPT_SHIM_NO_TRACE)) client->filter |= EVAL_SEND_TRACE; | |
142 | ||
143 | if (client->options & ASL_OPT_STDERR) | |
144 | { | |
145 | /* only add stderr if it is valid */ | |
146 | if (fcntl(STDERR_FILENO, F_GETFD) >= 0) | |
147 | { | |
148 | asl_client_add_output_file(client, fileno(stderr), ASL_MSG_FMT_STD, ASL_TIME_FMT_LCL, ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG), ASL_ENCODE_SAFE); | |
149 | } | |
150 | else | |
151 | { | |
152 | /* stderr has been closed, ignore ASL_OPT_STDERR flag */ | |
153 | client->options &= ~ASL_OPT_STDERR; | |
154 | } | |
155 | } | |
156 | ||
157 | return client; | |
158 | } | |
159 | ||
160 | asl_client_t * | |
161 | asl_client_open_from_file(int descriptor, const char *ident, const char *facility) | |
162 | { | |
163 | uint32_t status; | |
164 | asl_client_t *client = asl_client_open(ident, facility, 0); | |
165 | if (client == NULL) return NULL; | |
166 | ||
167 | client->filter = ASL_FILTER_MASK_UPTO(ASL_LEVEL_DEBUG); | |
168 | client->filter |= (EVAL_ACTIVE | EVAL_SEND_ASL); | |
169 | ||
170 | status = asl_file_open_write_fd(descriptor, &(client->aslfile)); | |
171 | if (status != ASL_STATUS_OK) | |
172 | { | |
173 | _asl_client_free_internal(client); | |
174 | return NULL; | |
175 | } | |
176 | ||
177 | client->aslfileid = 1; | |
178 | ||
179 | return client; | |
180 | } | |
181 | ||
182 | asl_client_t * | |
183 | asl_client_retain(asl_client_t *client) | |
184 | { | |
185 | if (client == NULL) return NULL; | |
186 | asl_retain((asl_object_t)client); | |
187 | return client; | |
188 | } | |
189 | ||
190 | void | |
191 | asl_client_release(asl_client_t *client) | |
192 | { | |
193 | if (client == NULL) return; | |
194 | asl_release((asl_object_t)client); | |
195 | } | |
196 | ||
197 | #pragma mark - | |
198 | #pragma mark database access | |
199 | ||
200 | ASL_STATUS | |
201 | asl_client_send(asl_client_t *client, asl_msg_t *msg) | |
202 | { | |
203 | return asl_client_internal_send((asl_object_t)client, (asl_object_t)msg, __builtin_return_address(0)); | |
204 | } | |
205 | ||
206 | static asl_msg_list_t * | |
207 | _do_server_match(asl_msg_list_t *qlist, size_t *last, size_t start, size_t count, uint32_t duration, int dir) | |
208 | { | |
209 | char *str, *res = NULL; | |
210 | uint32_t len, reslen, status; | |
211 | uint64_t last64, start64, count64; | |
212 | kern_return_t kstatus; | |
213 | asl_msg_list_t *out; | |
214 | caddr_t vmstr; | |
215 | mach_port_t asl_server_port = asl_core_get_service_port(0); | |
216 | ||
217 | if (asl_server_port == MACH_PORT_NULL) return NULL; | |
218 | ||
219 | str = NULL; | |
220 | if (qlist == NULL) | |
221 | { | |
222 | asprintf(&str, "0\n"); | |
223 | len = 3; | |
224 | } | |
225 | else | |
226 | { | |
227 | str = asl_msg_list_to_string(qlist, &len); | |
228 | } | |
229 | ||
230 | if (str == NULL) return NULL; | |
231 | ||
232 | kstatus = vm_allocate(mach_task_self(), (vm_address_t *)&vmstr, len, VM_FLAGS_ANYWHERE | VM_MAKE_TAG(VM_MEMORY_ASL)); | |
233 | if (kstatus != KERN_SUCCESS) return NULL; | |
234 | ||
235 | memmove(vmstr, str, len); | |
236 | free(str); | |
237 | ||
238 | last64 = 0; | |
239 | start64 = start; | |
240 | count64 = count; | |
241 | ||
242 | kstatus = _asl_server_match(asl_server_port, vmstr, len, start64, count64, duration, dir, (caddr_t *)&res, &reslen, &last64, (int *)&status); | |
243 | if (kstatus != KERN_SUCCESS) return NULL; | |
244 | ||
245 | *last = last64; | |
246 | out = asl_msg_list_from_string(res); | |
247 | vm_deallocate(mach_task_self(), (vm_address_t)res, reslen); | |
248 | ||
249 | return out; | |
250 | } | |
251 | ||
252 | static asl_msg_list_t * | |
253 | _do_server_search(asl_msg_t *q) | |
254 | { | |
255 | asl_msg_list_t *out; | |
256 | char *qstr, *str, *res = NULL; | |
257 | uint32_t len, reslen = 0, status = ASL_STATUS_OK; | |
258 | uint64_t cmax = 0; | |
259 | kern_return_t kstatus; | |
260 | caddr_t vmstr; | |
261 | mach_port_t asl_server_port = asl_core_get_service_port(0); | |
262 | ||
263 | if (asl_server_port == MACH_PORT_NULL) return NULL; | |
264 | ||
265 | len = 0; | |
266 | qstr = asl_msg_to_string(q, &len); | |
267 | ||
268 | str = NULL; | |
269 | if (qstr == NULL) | |
270 | { | |
271 | asprintf(&str, "0\n"); | |
272 | len = 3; | |
273 | } | |
274 | else | |
275 | { | |
276 | asprintf(&str, "1\n%s\n", qstr); | |
277 | len += 3; | |
278 | free(qstr); | |
279 | } | |
280 | ||
281 | if (str == NULL) return NULL; | |
282 | ||
283 | kstatus = vm_allocate(mach_task_self(), (vm_address_t *)&vmstr, len, VM_FLAGS_ANYWHERE | VM_MAKE_TAG(VM_MEMORY_ASL)); | |
284 | if (kstatus != KERN_SUCCESS) return NULL; | |
285 | ||
286 | memmove(vmstr, str, len); | |
287 | free(str); | |
288 | ||
289 | kstatus = _asl_server_query_2(asl_server_port, vmstr, len, 0, 0, 0, (caddr_t *)&res, &reslen, &cmax, (int *)&status); | |
290 | if (kstatus != KERN_SUCCESS) return NULL; | |
291 | ||
292 | out = asl_msg_list_from_string(res); | |
293 | vm_deallocate(mach_task_self(), (vm_address_t)res, reslen); | |
294 | ||
295 | return out; | |
296 | } | |
297 | ||
298 | static asl_msg_list_t * | |
299 | _do_store_match(asl_msg_list_t *qlist, size_t *last, size_t start, size_t count, uint32_t duration, int32_t direction) | |
300 | { | |
301 | asl_msg_list_t *out; | |
302 | uint32_t status; | |
303 | uint64_t l64 = 0, s64; | |
304 | asl_store_t *store = NULL; | |
305 | ||
306 | uint32_t len; | |
307 | char *str = asl_msg_list_to_string(qlist, &len); | |
308 | free(str); | |
309 | ||
310 | status = asl_store_open_read(NULL, &store); | |
311 | if (status != 0) return NULL; | |
312 | if (store == NULL) return NULL; | |
313 | ||
314 | s64 = start; | |
315 | out = asl_store_match(store, qlist, &l64, s64, count, duration, direction); | |
316 | *last = l64; | |
317 | ||
318 | asl_store_close(store); | |
319 | ||
320 | return out; | |
321 | } | |
322 | ||
323 | static asl_msg_list_t * | |
324 | _do_store_search(asl_msg_t *query) | |
325 | { | |
326 | asl_msg_list_t *out, *qlist = NULL; | |
327 | uint32_t status; | |
328 | uint16_t op; | |
329 | uint64_t last = 0, start = 0; | |
330 | asl_store_t *store = NULL; | |
331 | const char *val = NULL; | |
332 | ||
333 | /* check for "ASLMessageId >[=] n" and set start_id */ | |
334 | status = asl_msg_lookup(query, ASL_KEY_MSG_ID, &val, &op); | |
335 | if ((status == 0) && (val != NULL) && (op & ASL_QUERY_OP_GREATER)) | |
336 | { | |
337 | if (op & ASL_QUERY_OP_EQUAL) start = atoll(val); | |
338 | else start = atoll(val) + 1; | |
339 | } | |
340 | ||
341 | status = asl_store_open_read(NULL, &store); | |
342 | if (status != 0) return NULL; | |
343 | if (store == NULL) return NULL; | |
344 | ||
345 | if (query != NULL) | |
346 | { | |
347 | qlist = asl_msg_list_new(); | |
348 | asl_msg_list_append(qlist, query); | |
349 | } | |
350 | ||
351 | out = asl_store_match(store, qlist, &last, start, 0, 0, 1); | |
352 | asl_store_close(store); | |
353 | ||
354 | asl_msg_list_release(qlist); | |
355 | return out; | |
356 | } | |
357 | ||
358 | asl_msg_list_t * | |
359 | asl_client_match(asl_client_t *client, asl_msg_list_t *qlist, size_t *last, size_t start, size_t count, uint32_t duration, int32_t direction) | |
360 | { | |
361 | if (asl_store_location() == ASL_STORE_LOCATION_FILE) return _do_store_match(qlist, last, start, count, duration, direction); | |
362 | return _do_server_match(qlist, last, start, count, duration, direction); | |
363 | } | |
364 | ||
365 | asl_msg_list_t * | |
366 | asl_client_search(asl_client_t *client, asl_msg_t *query) | |
367 | { | |
368 | if (asl_store_location() == ASL_STORE_LOCATION_FILE) return _do_store_search(query); | |
369 | return _do_server_search(query); | |
370 | } | |
371 | ||
372 | ||
373 | #pragma mark - | |
374 | #pragma mark output control | |
375 | ||
376 | /* | |
377 | * Returns last filter value, or -1 on error. | |
378 | * Note that this allows ASL_FILTER_MASK_TUNNEL (0x100) to be set. | |
379 | * That is SPI that's used by some clients. | |
380 | */ | |
381 | int | |
382 | asl_client_set_filter(asl_client_t *client, int filter) | |
383 | { | |
384 | if (client == NULL) return -1; | |
385 | ||
386 | uint32_t allbits = client->filter; | |
387 | int last = allbits & (~EVAL_ACTION_MASK); | |
388 | client->filter = (allbits & EVAL_ACTION_MASK) | (filter & (~EVAL_ACTION_MASK)); | |
389 | return last; | |
390 | } | |
391 | ||
392 | /* SPI */ | |
393 | uint32_t | |
394 | asl_client_set_control(asl_client_t *client, uint32_t filter) | |
395 | { | |
396 | if (client == NULL) return UINT32_MAX; | |
397 | ||
398 | uint32_t last = client->filter; | |
399 | client->filter = filter; | |
400 | return last; | |
401 | } | |
402 | ||
403 | uint32_t | |
404 | asl_client_get_control(asl_client_t *client) | |
405 | { | |
406 | if (client == NULL) return UINT32_MAX; | |
407 | return client->filter; | |
408 | } | |
409 | ||
410 | ASL_STATUS | |
411 | asl_client_add_output_file(asl_client_t *client, int descriptor, const char *mfmt, const char *tfmt, int filter, int text_encoding) | |
412 | { | |
413 | uint32_t i; | |
414 | ||
415 | if (client == NULL) return ASL_STATUS_FAILED; | |
416 | ||
417 | for (i = 0; i < client->out_count; i++) | |
418 | { | |
419 | if (client->out_list[i].fd == descriptor) | |
420 | { | |
421 | /* update message format, time format, filter, and text encoding */ | |
422 | free(client->out_list[i].mfmt); | |
423 | client->out_list[i].mfmt = NULL; | |
424 | if (mfmt != NULL) client->out_list[i].mfmt = strdup(mfmt); | |
425 | ||
426 | free(client->out_list[i].tfmt); | |
427 | client->out_list[i].tfmt = NULL; | |
428 | if (tfmt != NULL) client->out_list[i].tfmt = strdup(tfmt); | |
429 | ||
430 | client->out_list[i].encoding = text_encoding; | |
431 | client->out_list[i].filter = filter; | |
432 | ||
433 | return ASL_STATUS_OK; | |
434 | } | |
435 | } | |
436 | ||
437 | if (client->out_count == 0) client->out_list = NULL; | |
438 | client->out_list = (asl_out_file_t *)reallocf(client->out_list, (1 + client->out_count) * sizeof(asl_out_file_t)); | |
439 | ||
440 | if (client->out_list == NULL) return ASL_STATUS_FAILED; | |
441 | ||
442 | client->out_list[client->out_count].fd = descriptor; | |
443 | client->out_list[client->out_count].encoding = text_encoding; | |
444 | client->out_list[client->out_count].filter = filter; | |
445 | client->out_list[client->out_count].mfmt = NULL; | |
446 | if (mfmt != NULL) client->out_list[client->out_count].mfmt = strdup(mfmt); | |
447 | client->out_list[client->out_count].tfmt = NULL; | |
448 | if (tfmt != NULL) client->out_list[client->out_count].tfmt = strdup(tfmt); | |
449 | ||
450 | client->out_count++; | |
451 | ||
452 | return ASL_STATUS_OK; | |
453 | } | |
454 | ||
455 | /* returns last filter value, or -1 on error */ | |
456 | int | |
457 | asl_client_set_output_file_filter(asl_client_t *client, int descriptor, int filter) | |
458 | { | |
459 | uint32_t i; | |
460 | int last = 0; | |
461 | ||
462 | if (client == NULL) return -1; | |
463 | ||
464 | for (i = 0; i < client->out_count; i++) | |
465 | { | |
466 | if (client->out_list[i].fd == descriptor) | |
467 | { | |
468 | /* update filter */ | |
469 | last = client->out_list[i].filter; | |
470 | client->out_list[i].filter = filter; | |
471 | break; | |
472 | } | |
473 | } | |
474 | ||
475 | return last; | |
476 | } | |
477 | ||
478 | ASL_STATUS | |
479 | asl_client_remove_output_file(asl_client_t *client, int descriptor) | |
480 | { | |
481 | uint32_t i; | |
482 | int x; | |
483 | ||
484 | if (client == NULL) return ASL_STATUS_INVALID_ARG; | |
485 | ||
486 | if (client->out_count == 0) return ASL_STATUS_OK; | |
487 | ||
488 | x = -1; | |
489 | for (i = 0; i < client->out_count; i++) | |
490 | { | |
491 | if (client->out_list[i].fd == descriptor) | |
492 | { | |
493 | x = i; | |
494 | break; | |
495 | } | |
496 | } | |
497 | ||
498 | if (x == -1) return ASL_STATUS_OK; | |
499 | ||
500 | free(client->out_list[x].mfmt); | |
501 | free(client->out_list[x].tfmt); | |
502 | ||
503 | for (i = x + 1; i < client->out_count; i++, x++) | |
504 | { | |
505 | client->out_list[x] = client->out_list[i]; | |
506 | } | |
507 | ||
508 | client->out_count--; | |
509 | ||
510 | if (client->out_count == 0) | |
511 | { | |
512 | free(client->out_list); | |
513 | client->out_list = NULL; | |
514 | } | |
515 | else | |
516 | { | |
517 | client->out_list = (asl_out_file_t *)reallocf(client->out_list, client->out_count * sizeof(asl_out_file_t)); | |
518 | ||
519 | if (client->out_list == NULL) | |
520 | { | |
521 | client->out_count = 0; | |
522 | return ASL_STATUS_FAILED; | |
523 | } | |
524 | } | |
525 | ||
526 | return ASL_STATUS_OK; | |
527 | } | |
528 | ||
529 | #pragma mark - | |
530 | #pragma mark dictionary access | |
531 | ||
532 | asl_msg_t * | |
533 | asl_client_kvdict(asl_client_t *client) | |
534 | { | |
535 | if (client == NULL) return NULL; | |
536 | return client->kvdict; | |
537 | } | |
538 | ||
539 | #pragma mark - | |
540 | #pragma mark asl_object support | |
541 | ||
542 | static void | |
543 | _jump_dealloc(asl_object_private_t *obj) | |
544 | { | |
545 | _asl_client_free_internal((asl_client_t *)obj); | |
546 | } | |
547 | ||
548 | static void | |
549 | _jump_append(asl_object_private_t *obj, asl_object_private_t *newobj, void *addr) | |
550 | { | |
551 | int type = asl_get_type((asl_object_t)newobj); | |
552 | ||
553 | if (type == ASL_TYPE_LIST) | |
554 | { | |
555 | asl_msg_t *msg; | |
556 | asl_msg_list_reset_iteration((asl_msg_list_t *)newobj, 0); | |
557 | while (NULL != (msg = asl_msg_list_next((asl_msg_list_t *)newobj))) | |
558 | { | |
559 | if (asl_client_internal_send((asl_object_t)obj, (asl_object_t)msg, addr) != ASL_STATUS_OK) return; | |
560 | } | |
561 | } | |
562 | else if ((type == ASL_TYPE_MSG) || (type == ASL_TYPE_QUERY)) | |
563 | { | |
564 | asl_client_internal_send((asl_object_t)obj, (asl_object_t)newobj, addr); | |
565 | } | |
566 | } | |
567 | ||
568 | static asl_object_private_t * | |
569 | _jump_search(asl_object_private_t *obj, asl_object_private_t *query) | |
570 | { | |
571 | int type = asl_get_type((asl_object_t)query); | |
572 | if ((query != NULL) && (type != ASL_TYPE_MSG) && (type != ASL_TYPE_QUERY)) return NULL; | |
573 | ||
574 | return (asl_object_private_t *)asl_client_search((asl_client_t *)obj, (asl_msg_t *)query); | |
575 | } | |
576 | ||
577 | static asl_object_private_t * | |
578 | _jump_match(asl_object_private_t *obj, asl_object_private_t *qlist, size_t *last, size_t start, size_t count, uint32_t duration, int32_t dir) | |
579 | { | |
580 | asl_msg_list_t *out = NULL; | |
581 | int type = asl_get_type((asl_object_t)qlist); | |
582 | ||
583 | if ((qlist != NULL) && (type != ASL_TYPE_LIST)) return NULL; | |
584 | ||
585 | out = asl_client_match((asl_client_t *)obj, (asl_msg_list_t *)qlist, last, start, count, duration, dir); | |
586 | return (asl_object_private_t *)out; | |
587 | } | |
588 | ||
589 | __private_extern__ const asl_jump_table_t * | |
590 | asl_client_jump_table() | |
591 | { | |
592 | static const asl_jump_table_t jump = | |
593 | { | |
594 | .alloc = NULL, | |
595 | .dealloc = &_jump_dealloc, | |
596 | .set_key_val_op = NULL, | |
597 | .unset_key = NULL, | |
598 | .get_val_op_for_key = NULL, | |
599 | .get_key_val_op_at_index = NULL, | |
600 | .count = NULL, | |
601 | .next = NULL, | |
602 | .prev = NULL, | |
603 | .get_object_at_index = NULL, | |
604 | .set_iteration_index = NULL, | |
605 | .remove_object_at_index = NULL, | |
606 | .append = &_jump_append, | |
607 | .prepend = NULL, | |
608 | .search = &_jump_search, | |
609 | .match = &_jump_match | |
610 | }; | |
611 | ||
612 | return &jump; | |
613 | } | |
614 |