Print this page
10132 smatch fixes for MDB
Reviewed by: Andy Fiddaman <andy@omniosce.org>
Split |
Close |
Expand all |
Collapse all |
--- old/usr/src/cmd/mdb/common/mdb/mdb_tab.c
+++ new/usr/src/cmd/mdb/common/mdb/mdb_tab.c
1 1 /*
2 2 * CDDL HEADER START
3 3 *
4 4 * The contents of this file are subject to the terms of the
5 5 * Common Development and Distribution License (the "License").
6 6 * You may not use this file except in compliance with the License.
7 7 *
8 8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9 9 * or http://www.opensolaris.org/os/licensing.
10 10 * See the License for the specific language governing permissions
11 11 * and limitations under the License.
12 12 *
↓ open down ↓ |
12 lines elided |
↑ open up ↑ |
13 13 * When distributing Covered Code, include this CDDL HEADER in each
14 14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15 15 * If applicable, add the following below this CDDL HEADER, with the
16 16 * fields enclosed by brackets "[]" replaced with your own identifying
17 17 * information: Portions Copyright [yyyy] [name of copyright owner]
18 18 *
19 19 * CDDL HEADER END
20 20 */
21 21 /*
22 22 * Copyright (c) 2013 by Delphix. All rights reserved.
23 - * Copyright (c) 2012 Joyent, Inc. All rights reserved.
23 + * Copyright (c) 2018, Joyent, Inc.
24 24 * Copyright (c) 2013 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
25 25 */
26 26 /*
27 27 * This file contains all of the interfaces for mdb's tab completion engine.
28 28 * Currently some interfaces are private to mdb and its internal implementation,
29 29 * those are in mdb_tab.h. Other pieces are public interfaces. Those are in
30 30 * mdb_modapi.h.
31 31 *
32 32 * Memory allocations in tab completion context have to be done very carefully.
33 33 * We need to think of ourselves as the same as any other command that is being
34 34 * executed by the user, which means we must use UM_GC to handle being
35 35 * interrupted.
36 36 */
37 37
38 38 #include <mdb/mdb_modapi.h>
39 39 #include <mdb/mdb_ctf.h>
40 40 #include <mdb/mdb_ctf_impl.h>
41 41 #include <mdb/mdb_string.h>
42 42 #include <mdb/mdb_module.h>
43 43 #include <mdb/mdb_debug.h>
44 44 #include <mdb/mdb_print.h>
45 45 #include <mdb/mdb_nv.h>
46 46 #include <mdb/mdb_tab.h>
47 47 #include <mdb/mdb_target.h>
48 48 #include <mdb/mdb.h>
49 49
50 50 #include <ctype.h>
51 51
52 52 /*
53 53 * There may be another way to do this, but this works well enough.
54 54 */
55 55 #define COMMAND_SEPARATOR "::"
56 56
57 57 /*
58 58 * find_command_start --
59 59 *
60 60 * Given a buffer find the start of the last command.
61 61 */
62 62 static char *
63 63 tab_find_command_start(char *buf)
64 64 {
65 65 char *offset = strstr(buf, COMMAND_SEPARATOR);
66 66
67 67 if (offset == NULL)
68 68 return (NULL);
69 69
70 70 for (;;) {
71 71 char *next = strstr(offset + strlen(COMMAND_SEPARATOR),
72 72 COMMAND_SEPARATOR);
73 73
74 74 if (next == NULL) {
75 75 return (offset);
76 76 }
77 77
78 78 offset = next;
79 79 }
80 80 }
81 81
82 82 /*
83 83 * get_dcmd --
84 84 *
85 85 * Given a buffer containing a command and its argument return
86 86 * the name of the command and the offset in the buffer where
87 87 * the command arguments start.
88 88 *
89 89 * Note: This will modify the buffer.
90 90 */
91 91 char *
92 92 tab_get_dcmd(char *buf, char **args, uint_t *flags)
93 93 {
94 94 char *start = buf + strlen(COMMAND_SEPARATOR);
95 95 char *separator = start;
96 96 const char *end = buf + strlen(buf);
97 97 uint_t space = 0;
98 98
99 99 while (separator < end && !isspace(*separator))
100 100 separator++;
101 101
102 102 if (separator == end) {
103 103 *args = NULL;
104 104 } else {
105 105 if (isspace(*separator))
106 106 space = 1;
107 107
108 108 *separator++ = '\0';
109 109 *args = separator;
110 110 }
111 111
112 112 if (space)
113 113 *flags |= DCMD_TAB_SPACE;
114 114
115 115 return (start);
116 116 }
117 117
118 118 /*
119 119 * count_args --
120 120 *
121 121 * Given a buffer containing dmcd arguments return the total number
122 122 * of arguments.
123 123 *
124 124 * While parsing arguments we need to keep track of whether or not the last
125 125 * arguments ends with a trailing space.
126 126 */
127 127 static int
128 128 tab_count_args(const char *input, uint_t *flags)
129 129 {
130 130 const char *index;
131 131 int argc = 0;
132 132 uint_t space = *flags & DCMD_TAB_SPACE;
133 133 index = input;
134 134
135 135 while (*index != '\0') {
136 136 while (*index != '\0' && isspace(*index)) {
137 137 index++;
138 138 space = 1;
139 139 }
140 140
141 141 if (*index != '\0' && !isspace(*index)) {
142 142 argc++;
143 143 space = 0;
144 144 while (*index != '\0' && !isspace (*index)) {
145 145 index++;
146 146 }
147 147 }
148 148 }
149 149
150 150 if (space)
151 151 *flags |= DCMD_TAB_SPACE;
152 152 else
153 153 *flags &= ~DCMD_TAB_SPACE;
154 154
155 155 return (argc);
156 156 }
157 157
158 158 /*
159 159 * copy_args --
160 160 *
161 161 * Given a buffer containing dcmd arguments and an array of mdb_arg_t's
162 162 * initialize the string value of each mdb_arg_t.
163 163 *
164 164 * Note: This will modify the buffer.
165 165 */
166 166 static int
167 167 tab_copy_args(char *input, int argc, mdb_arg_t *argv)
168 168 {
169 169 int i = 0;
170 170 char *index;
171 171
172 172 index = input;
173 173
174 174 while (*index) {
175 175 while (*index && isspace(*index)) {
176 176 index++;
177 177 }
178 178
179 179 if (*index && !isspace(*index)) {
180 180 char *end = index;
181 181
182 182 while (*end && !isspace(*end)) {
183 183 end++;
184 184 }
185 185
186 186 if (*end) {
187 187 *end++ = '\0';
188 188 }
189 189
190 190 argv[i].a_type = MDB_TYPE_STRING;
191 191 argv[i].a_un.a_str = index;
192 192
193 193 index = end;
194 194 i++;
195 195 }
196 196 }
197 197
198 198 if (i != argc)
199 199 return (-1);
200 200
201 201 return (0);
202 202 }
203 203
204 204 /*
205 205 * parse-buf --
206 206 *
207 207 * Parse the given buffer and return the specified dcmd, the number
208 208 * of arguments, and array of mdb_arg_t containing the argument
209 209 * values.
210 210 *
211 211 * Note: this will modify the specified buffer. Caller is responisble
212 212 * for freeing argvp.
213 213 */
214 214 static int
215 215 tab_parse_buf(char *buf, char **dcmdp, int *argcp, mdb_arg_t **argvp,
216 216 uint_t *flags)
217 217 {
218 218 char *data = tab_find_command_start(buf);
219 219 char *args_data = NULL;
220 220 char *dcmd = NULL;
221 221 int argc = 0;
222 222 mdb_arg_t *argv = NULL;
223 223
224 224 if (data == NULL) {
225 225 return (-1);
226 226 }
227 227
228 228 dcmd = tab_get_dcmd(data, &args_data, flags);
229 229
230 230 if (dcmd == NULL) {
231 231 return (-1);
232 232 }
233 233
234 234 if (args_data != NULL) {
235 235 argc = tab_count_args(args_data, flags);
236 236
237 237 if (argc != 0) {
238 238 argv = mdb_alloc(sizeof (mdb_arg_t) * argc,
239 239 UM_SLEEP | UM_GC);
240 240
241 241 if (tab_copy_args(args_data, argc, argv) == -1)
242 242 return (-1);
243 243 }
244 244 }
245 245
246 246 *dcmdp = dcmd;
247 247 *argcp = argc;
248 248 *argvp = argv;
249 249
250 250 return (0);
251 251 }
252 252
253 253 /*
254 254 * tab_command --
255 255 *
256 256 * This function is executed anytime a tab is entered. It checks
257 257 * the current buffer to determine if there is a valid dmcd,
258 258 * if that dcmd has a tab completion handler it will invoke it.
259 259 *
260 260 * This function returns the string (if any) that should be added to the
261 261 * existing buffer to complete it.
262 262 */
263 263 int
264 264 mdb_tab_command(mdb_tab_cookie_t *mcp, const char *buf)
265 265 {
266 266 char *data;
267 267 char *dcmd = NULL;
268 268 int argc = 0;
269 269 mdb_arg_t *argv = NULL;
270 270 int ret = 0;
271 271 mdb_idcmd_t *cp;
272 272 uint_t flags = 0;
273 273
274 274 /*
275 275 * Parsing the command and arguments will modify the buffer
276 276 * (replacing spaces with \0), so make a copy of the specified
277 277 * buffer first.
278 278 */
279 279 data = mdb_alloc(strlen(buf) + 1, UM_SLEEP | UM_GC);
280 280 (void) strcpy(data, buf);
281 281
282 282 /*
283 283 * Get the specified dcmd and arguments from the buffer.
284 284 */
285 285 ret = tab_parse_buf(data, &dcmd, &argc, &argv, &flags);
286 286
287 287 /*
288 288 * Match against global symbols if the input is not a dcmd
289 289 */
290 290 if (ret != 0) {
291 291 (void) mdb_tab_complete_global(mcp, buf);
292 292 goto out;
293 293 }
294 294
295 295 /*
296 296 * Check to see if the buffer contains a valid dcmd
297 297 */
298 298 cp = mdb_dcmd_lookup(dcmd);
299 299
300 300 /*
301 301 * When argc is zero it indicates that we are trying to tab complete
302 302 * a dcmd or a global symbol. Note, that if there isn't the start of
303 303 * a dcmd, i.e. ::, then we will have already bailed in the call to
304 304 * tab_parse_buf.
305 305 */
306 306 if (cp == NULL && argc != 0) {
307 307 goto out;
308 308 }
309 309
310 310 /*
311 311 * Invoke the command specific tab completion handler or the built in
312 312 * dcmd one if there is no dcmd.
313 313 */
314 314 if (cp == NULL)
315 315 (void) mdb_tab_complete_dcmd(mcp, dcmd);
316 316 else
317 317 mdb_call_tab(cp, mcp, flags, argc, argv);
318 318
319 319 out:
320 320 return (mdb_tab_size(mcp));
321 321 }
322 322
323 323 static int
324 324 tab_complete_dcmd(mdb_var_t *v, void *arg)
325 325 {
326 326 mdb_idcmd_t *idcp = mdb_nv_get_cookie(mdb_nv_get_cookie(v));
327 327 mdb_tab_cookie_t *mcp = (mdb_tab_cookie_t *)arg;
328 328
329 329 /*
330 330 * The way that mdb is implemented, even commands like $C will show up
331 331 * here. As such, we don't want to match anything that doesn't start
332 332 * with an alpha or number. While nothing currently appears (via a
333 333 * cursory search with mdb -k) to start with a capital letter or a
334 334 * number, we'll support them anyways.
335 335 */
336 336 if (!isalnum(idcp->idc_name[0]))
337 337 return (0);
338 338
339 339 mdb_tab_insert(mcp, idcp->idc_name);
340 340 return (0);
341 341 }
342 342
343 343 int
344 344 mdb_tab_complete_dcmd(mdb_tab_cookie_t *mcp, const char *dcmd)
345 345 {
346 346 if (dcmd != NULL)
347 347 mdb_tab_setmbase(mcp, dcmd);
348 348 mdb_nv_sort_iter(&mdb.m_dcmds, tab_complete_dcmd, mcp,
349 349 UM_GC | UM_SLEEP);
350 350 return (0);
351 351 }
352 352
353 353 static int
354 354 tab_complete_walker(mdb_var_t *v, void *arg)
355 355 {
356 356 mdb_iwalker_t *iwp = mdb_nv_get_cookie(mdb_nv_get_cookie(v));
357 357 mdb_tab_cookie_t *mcp = arg;
358 358
359 359 mdb_tab_insert(mcp, iwp->iwlk_name);
360 360 return (0);
361 361 }
362 362
363 363 int
364 364 mdb_tab_complete_walker(mdb_tab_cookie_t *mcp, const char *walker)
365 365 {
366 366 if (walker != NULL)
367 367 mdb_tab_setmbase(mcp, walker);
368 368 mdb_nv_sort_iter(&mdb.m_walkers, tab_complete_walker, mcp,
369 369 UM_GC | UM_SLEEP);
370 370
371 371 return (0);
372 372 }
373 373
374 374 mdb_tab_cookie_t *
375 375 mdb_tab_init(void)
376 376 {
377 377 mdb_tab_cookie_t *mcp;
378 378
379 379 mcp = mdb_zalloc(sizeof (mdb_tab_cookie_t), UM_SLEEP | UM_GC);
380 380 (void) mdb_nv_create(&mcp->mtc_nv, UM_SLEEP | UM_GC);
381 381
382 382 return (mcp);
383 383 }
384 384
385 385 size_t
386 386 mdb_tab_size(mdb_tab_cookie_t *mcp)
387 387 {
388 388 return (mdb_nv_size(&mcp->mtc_nv));
389 389 }
390 390
391 391 /*
392 392 * Determine whether the specified name is a valid tab completion for
393 393 * the given command. If the name is a valid tab completion then
394 394 * it will be saved in the mdb_tab_cookie_t.
395 395 */
↓ open down ↓ |
362 lines elided |
↑ open up ↑ |
396 396 void
397 397 mdb_tab_insert(mdb_tab_cookie_t *mcp, const char *name)
398 398 {
399 399 size_t matches, index;
400 400 mdb_var_t *v;
401 401
402 402 /*
403 403 * If we have a match set, then we want to verify that we actually match
404 404 * it.
405 405 */
406 - if (mcp->mtc_base != NULL &&
407 - strncmp(name, mcp->mtc_base, strlen(mcp->mtc_base)) != 0)
406 + if (strncmp(name, mcp->mtc_base, strlen(mcp->mtc_base)) != 0)
408 407 return;
409 408
410 409 v = mdb_nv_lookup(&mcp->mtc_nv, name);
411 410 if (v != NULL)
412 411 return;
413 412
414 413 (void) mdb_nv_insert(&mcp->mtc_nv, name, NULL, 0, MDB_NV_RDONLY);
415 414
416 415 matches = mdb_tab_size(mcp);
417 416 if (matches == 1) {
418 417 (void) strlcpy(mcp->mtc_match, name, MDB_SYM_NAMLEN);
419 418 } else {
420 419 index = 0;
421 420 while (mcp->mtc_match[index] &&
422 421 mcp->mtc_match[index] == name[index])
423 422 index++;
424 423
425 424 mcp->mtc_match[index] = '\0';
426 425 }
427 426 }
428 427
429 428 /*ARGSUSED*/
430 429 static int
431 430 tab_print_cb(mdb_var_t *v, void *ignored)
432 431 {
433 432 mdb_printf("%s\n", mdb_nv_get_name(v));
434 433 return (0);
435 434 }
↓ open down ↓ |
18 lines elided |
↑ open up ↑ |
436 435
437 436 void
438 437 mdb_tab_print(mdb_tab_cookie_t *mcp)
439 438 {
440 439 mdb_nv_sort_iter(&mcp->mtc_nv, tab_print_cb, NULL, UM_SLEEP | UM_GC);
441 440 }
442 441
443 442 const char *
444 443 mdb_tab_match(mdb_tab_cookie_t *mcp)
445 444 {
446 - size_t blen;
447 -
448 - if (mcp->mtc_base == NULL)
449 - blen = 0;
450 - else
451 - blen = strlen(mcp->mtc_base);
452 - return (mcp->mtc_match + blen);
445 + return (mcp->mtc_match + strlen(mcp->mtc_base));
453 446 }
454 447
455 448 void
456 449 mdb_tab_setmbase(mdb_tab_cookie_t *mcp, const char *base)
457 450 {
458 451 (void) strlcpy(mcp->mtc_base, base, MDB_SYM_NAMLEN);
459 452 }
460 453
461 454 /*
462 455 * This function is currently a no-op due to the fact that we have to GC because
463 456 * we're in command context.
464 457 */
465 458 /*ARGSUSED*/
466 459 void
467 460 mdb_tab_fini(mdb_tab_cookie_t *mcp)
468 461 {
469 462 }
470 463
471 464 /*ARGSUSED*/
472 465 static int
473 466 tab_complete_global(void *arg, const GElf_Sym *sym, const char *name,
474 467 const mdb_syminfo_t *sip, const char *obj)
475 468 {
476 469 mdb_tab_cookie_t *mcp = arg;
477 470 mdb_tab_insert(mcp, name);
478 471 return (0);
479 472 }
480 473
481 474 /*
482 475 * This function tab completes against all loaded global symbols.
483 476 */
484 477 int
485 478 mdb_tab_complete_global(mdb_tab_cookie_t *mcp, const char *name)
486 479 {
487 480 mdb_tab_setmbase(mcp, name);
488 481 (void) mdb_tgt_symbol_iter(mdb.m_target, MDB_TGT_OBJ_EVERY,
489 482 MDB_TGT_SYMTAB, MDB_TGT_BIND_ANY | MDB_TGT_TYPE_OBJECT |
490 483 MDB_TGT_TYPE_FUNC, tab_complete_global, mcp);
491 484 return (0);
492 485 }
493 486
494 487 /*
495 488 * This function takes a ctf id and determines whether or not the associated
496 489 * type should be considered as a potential match for the given tab
497 490 * completion command. We verify that the type itself is valid
498 491 * for completion given the current context of the command, resolve
499 492 * its actual name, and then pass it off to mdb_tab_insert to determine
500 493 * if it's an actual match.
501 494 */
502 495 static int
503 496 tab_complete_type(mdb_ctf_id_t id, void *arg)
504 497 {
505 498 int rkind;
506 499 char buf[MDB_SYM_NAMLEN];
507 500 mdb_ctf_id_t rid;
508 501 mdb_tab_cookie_t *mcp = arg;
509 502 uint_t flags = (uint_t)(uintptr_t)mcp->mtc_cba;
510 503
511 504 /*
512 505 * CTF data includes types that mdb commands don't understand. Before
513 506 * we resolve the actual type prune any entry that is a type we
514 507 * don't care about.
515 508 */
516 509 switch (mdb_ctf_type_kind(id)) {
517 510 case CTF_K_CONST:
518 511 case CTF_K_RESTRICT:
519 512 case CTF_K_VOLATILE:
520 513 return (0);
521 514 }
522 515
523 516 if (mdb_ctf_type_resolve(id, &rid) != 0)
524 517 return (1);
525 518
526 519 rkind = mdb_ctf_type_kind(rid);
527 520
528 521 if ((flags & MDB_TABC_MEMBERS) && rkind != CTF_K_STRUCT &&
529 522 rkind != CTF_K_UNION)
530 523 return (0);
531 524
532 525 if ((flags & MDB_TABC_NOPOINT) && rkind == CTF_K_POINTER)
533 526 return (0);
534 527
535 528 if ((flags & MDB_TABC_NOARRAY) && rkind == CTF_K_ARRAY)
536 529 return (0);
537 530
538 531 (void) mdb_ctf_type_name(id, buf, sizeof (buf));
539 532
540 533 mdb_tab_insert(mcp, buf);
541 534 return (0);
542 535 }
543 536
544 537 /*ARGSUSED*/
545 538 static int
546 539 mdb_tab_complete_module(void *data, const mdb_map_t *mp, const char *name)
547 540 {
548 541 (void) mdb_ctf_type_iter(name, tab_complete_type, data);
549 542 return (0);
550 543 }
551 544
552 545 int
553 546 mdb_tab_complete_type(mdb_tab_cookie_t *mcp, const char *name, uint_t flags)
554 547 {
555 548 mdb_tgt_t *t = mdb.m_target;
556 549
557 550 mcp->mtc_cba = (void *)(uintptr_t)flags;
558 551 if (name != NULL)
559 552 mdb_tab_setmbase(mcp, name);
560 553
561 554 (void) mdb_tgt_object_iter(t, mdb_tab_complete_module, mcp);
562 555 (void) mdb_ctf_type_iter(MDB_CTF_SYNTHETIC_ITER, tab_complete_type,
563 556 mcp);
564 557 return (0);
565 558 }
566 559
567 560 /*ARGSUSED*/
568 561 static int
569 562 tab_complete_member(const char *name, mdb_ctf_id_t id, ulong_t off, void *arg)
570 563 {
571 564 mdb_tab_cookie_t *mcp = arg;
572 565 mdb_tab_insert(mcp, name);
573 566 return (0);
574 567 }
575 568
576 569 int
577 570 mdb_tab_complete_member_by_id(mdb_tab_cookie_t *mcp, mdb_ctf_id_t id,
578 571 const char *member)
579 572 {
580 573 if (member != NULL)
581 574 mdb_tab_setmbase(mcp, member);
582 575 (void) mdb_ctf_member_iter(id, tab_complete_member, mcp);
583 576 return (0);
584 577 }
585 578
586 579 int
587 580 mdb_tab_complete_member(mdb_tab_cookie_t *mcp, const char *type,
588 581 const char *member)
589 582 {
590 583 mdb_ctf_id_t id;
591 584
592 585 if (mdb_ctf_lookup_by_name(type, &id) != 0)
593 586 return (-1);
594 587
595 588 return (mdb_tab_complete_member_by_id(mcp, id, member));
596 589 }
597 590
598 591 int
599 592 mdb_tab_complete_mt(mdb_tab_cookie_t *mcp, uint_t flags, int argc,
600 593 const mdb_arg_t *argv)
601 594 {
602 595 char tn[MDB_SYM_NAMLEN];
603 596 int ret;
604 597
605 598 if (argc == 0 && !(flags & DCMD_TAB_SPACE))
606 599 return (0);
607 600
608 601 if (argc == 0)
609 602 return (mdb_tab_complete_type(mcp, NULL, MDB_TABC_MEMBERS));
610 603
611 604 if ((ret = mdb_tab_typename(&argc, &argv, tn, sizeof (tn))) < 0)
612 605 return (ret);
613 606
614 607 if (argc == 1 && (!(flags & DCMD_TAB_SPACE) || ret == 1))
615 608 return (mdb_tab_complete_type(mcp, tn, MDB_TABC_MEMBERS));
616 609
617 610 if (argc == 1 && (flags & DCMD_TAB_SPACE))
618 611 return (mdb_tab_complete_member(mcp, tn, NULL));
619 612
620 613 if (argc == 2)
621 614 return (mdb_tab_complete_member(mcp, tn, argv[1].a_un.a_str));
622 615
623 616 return (0);
624 617 }
625 618
626 619 /*
627 620 * This is similar to mdb_print.c's args_to_typename, but it has subtle
628 621 * differences surrounding how the strings of one element are handled that have
629 622 * 'struct', 'enum', or 'union' in them and instead works with them for tab
630 623 * completion purposes.
631 624 */
632 625 int
633 626 mdb_tab_typename(int *argcp, const mdb_arg_t **argvp, char *buf, size_t len)
634 627 {
635 628 int argc = *argcp;
636 629 const mdb_arg_t *argv = *argvp;
637 630
638 631 if (argc < 1 || argv->a_type != MDB_TYPE_STRING)
639 632 return (DCMD_USAGE);
640 633
641 634 if (strcmp(argv->a_un.a_str, "struct") == 0 ||
642 635 strcmp(argv->a_un.a_str, "enum") == 0 ||
643 636 strcmp(argv->a_un.a_str, "union") == 0) {
644 637 if (argc == 1) {
645 638 (void) mdb_snprintf(buf, len, "%s ",
646 639 argv[0].a_un.a_str);
647 640 return (1);
648 641 }
649 642
650 643 if (argv[1].a_type != MDB_TYPE_STRING)
651 644 return (DCMD_USAGE);
652 645
653 646 (void) mdb_snprintf(buf, len, "%s %s",
654 647 argv[0].a_un.a_str, argv[1].a_un.a_str);
655 648
656 649 *argcp = argc - 1;
657 650 *argvp = argv + 1;
658 651 } else {
659 652 (void) mdb_snprintf(buf, len, "%s", argv[0].a_un.a_str);
660 653 }
661 654
662 655 return (0);
663 656 }
↓ open down ↓ |
201 lines elided |
↑ open up ↑ |
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX