Changes in uspace/srv/fs/cdfs/cdfs_ops.c [44ecf89:fc22069] in mainline
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
uspace/srv/fs/cdfs/cdfs_ops.c
r44ecf89 rfc22069 39 39 #include "cdfs_ops.h" 40 40 #include <stdbool.h> 41 #include <adt/list.h> 41 42 #include <adt/hash_table.h> 42 43 #include <adt/hash.h> … … 47 48 #include <errno.h> 48 49 #include <block.h> 50 #include <scsi/mmc.h> 49 51 #include <str.h> 50 52 #include <byteorder.h> … … 65 67 66 68 #define CDFS_STANDARD_IDENT "CD001" 69 70 enum { 71 CDFS_NAME_CURDIR = '\x00', 72 CDFS_NAME_PARENTDIR = '\x01' 73 }; 67 74 68 75 typedef enum { … … 126 133 127 134 typedef struct { 128 uint8_t res0;135 uint8_t flags; /* reserved in primary */ 129 136 130 137 uint8_t system_ident[32]; … … 134 141 uint32_t_lb lba_size; 135 142 136 uint8_t res2[32];143 uint8_t esc_seq[32]; /* reserved in primary */ 137 144 uint16_t_lb set_size; 138 145 uint16_t_lb sequence_nr; … … 164 171 165 172 uint8_t fs_version; 166 } __attribute__((packed)) cdfs_vol_desc_pri mary_t;173 } __attribute__((packed)) cdfs_vol_desc_prisec_t; 167 174 168 175 typedef struct { … … 172 179 union { 173 180 cdfs_vol_desc_boot_t boot; 174 cdfs_vol_desc_pri mary_t primary;181 cdfs_vol_desc_prisec_t prisec; 175 182 } data; 176 183 } __attribute__((packed)) cdfs_vol_desc_t; 184 185 typedef enum { 186 /** ASCII character set / encoding (base ISO 9660) */ 187 enc_ascii, 188 /** UCS-2 character set / encoding (Joliet) */ 189 enc_ucs2 190 } cdfs_enc_t; 177 191 178 192 typedef enum { … … 191 205 192 206 typedef struct { 207 link_t link; /**< Link to list of all instances */ 208 service_id_t service_id; /**< Service ID of block device */ 209 cdfs_enc_t enc; /**< Filesystem string encoding */ 210 } cdfs_t; 211 212 typedef struct { 193 213 fs_node_t *fs_node; /**< FS node */ 194 214 fs_index_t index; /**< Node index */ 195 service_id_t service_id; /**< Service ID of block device*/215 cdfs_t *fs; /**< File system */ 196 216 197 217 ht_link_t nh_link; /**< Nodes hash table link */ … … 207 227 } cdfs_node_t; 208 228 229 /** String encoding */ 230 enum { 231 /** ASCII - standard ISO 9660 */ 232 ucs2_esc_seq_no = 3, 233 /** USC-2 - Joliet */ 234 ucs2_esc_seq_len = 3 235 }; 236 237 /** Joliet SVD UCS-2 escape sequences */ 238 static uint8_t ucs2_esc_seq[ucs2_esc_seq_no][ucs2_esc_seq_len] = { 239 { 0x25, 0x2f, 0x40 }, 240 { 0x25, 0x2f, 0x43 }, 241 { 0x25, 0x2f, 0x45 } 242 }; 243 244 /** List of all instances */ 245 static LIST_INITIALIZE(cdfs_instances); 246 209 247 /** Shared index of nodes */ 210 248 static fs_index_t cdfs_index = 1; … … 234 272 { 235 273 cdfs_node_t *node = hash_table_get_inst(item, cdfs_node_t, nh_link); 236 return hash_combine(node-> service_id, node->index);274 return hash_combine(node->fs->service_id, node->index); 237 275 } 238 276 … … 242 280 ht_key_t *key = (ht_key_t*)k; 243 281 244 return key->service_id == node-> service_id && key->index == node->index;282 return key->service_id == node->fs->service_id && key->index == node->index; 245 283 } 246 284 … … 249 287 cdfs_node_t *node = hash_table_get_inst(item, cdfs_node_t, nh_link); 250 288 251 assert(node->type == CDFS_DIRECTORY);252 253 link_t *link;254 while ((link = list_first(&node->cs_list)) != NULL) {255 cdfs_dentry_t *dentry = list_get_instance(link, cdfs_dentry_t,link);256 list_remove(&dentry->link);257 free(dentry);289 if (node->type == CDFS_DIRECTORY) { 290 link_t *link; 291 while ((link = list_first(&node->cs_list)) != NULL) { 292 cdfs_dentry_t *dentry = list_get_instance(link, cdfs_dentry_t, link); 293 list_remove(&dentry->link); 294 free(dentry); 295 } 258 296 } 259 297 … … 300 338 node->fs_node = NULL; 301 339 node->index = 0; 302 node-> service_id = 0;340 node->fs = NULL; 303 341 node->type = CDFS_NONE; 304 342 node->lnkcnt = 0; … … 311 349 } 312 350 313 static int create_node(fs_node_t **rfn, service_id_t service_id, int lflag,351 static int create_node(fs_node_t **rfn, cdfs_t *fs, int lflag, 314 352 fs_index_t index) 315 353 { … … 332 370 333 371 fs_node_t *rootfn; 334 int rc = cdfs_root_get(&rootfn, service_id);372 int rc = cdfs_root_get(&rootfn, fs->service_id); 335 373 336 374 assert(rc == EOK); … … 341 379 node->index = index; 342 380 343 node-> service_id = service_id;381 node->fs = fs; 344 382 345 383 if (lflag & L_DIRECTORY) … … 365 403 366 404 /* Check for duplicate entries */ 367 list_foreach(parent->cs_list, link) { 368 cdfs_dentry_t *dentry = 369 list_get_instance(link, cdfs_dentry_t, link); 370 405 list_foreach(parent->cs_list, link, cdfs_dentry_t, dentry) { 371 406 if (str_cmp(dentry->name, name) == 0) 372 407 return EEXIST; … … 394 429 } 395 430 396 static bool cdfs_readdir(service_id_t service_id, fs_node_t *fs_node) 431 /** Decode CDFS string. 432 * 433 * @param data Pointer to string data 434 * @param dsize Size of data in bytes 435 * @param enc String encoding 436 * @return Decoded string 437 */ 438 static char *cdfs_decode_str(void *data, size_t dsize, cdfs_enc_t enc) 439 { 440 int rc; 441 char *str; 442 uint16_t *buf; 443 444 switch (enc) { 445 case enc_ascii: 446 str = malloc(dsize + 1); 447 if (str == NULL) 448 return NULL; 449 memcpy(str, data, dsize); 450 str[dsize] = '\0'; 451 break; 452 case enc_ucs2: 453 buf = calloc(dsize + 2, 1); 454 if (buf == NULL) 455 return NULL; 456 457 size_t i; 458 for (i = 0; i < dsize / sizeof(uint16_t); i++) { 459 buf[i] = uint16_t_be2host(((uint16_t *)data)[i]); 460 } 461 462 size_t dstr_size = dsize / sizeof(uint16_t) * 4 + 1; 463 str = malloc(dstr_size); 464 if (str == NULL) 465 return NULL; 466 467 rc = utf16_to_str(str, dstr_size, buf); 468 free(buf); 469 470 if (rc != EOK) 471 return NULL; 472 break; 473 default: 474 assert(false); 475 str = NULL; 476 } 477 478 return str; 479 } 480 481 /** Decode file name. 482 * 483 * @param data File name buffer 484 * @param dsize Fine name buffer size 485 * @param dtype Directory entry type 486 * @return Decoded file name (allocated string) 487 */ 488 static char *cdfs_decode_name(void *data, size_t dsize, cdfs_enc_t enc, 489 cdfs_dentry_type_t dtype) 490 { 491 char *name; 492 char *dot; 493 char *scolon; 494 495 name = cdfs_decode_str(data, dsize, enc); 496 if (name == NULL) 497 return NULL; 498 499 if (dtype == CDFS_DIRECTORY) 500 return name; 501 502 dot = str_chr(name, '.'); 503 504 if (dot != NULL) { 505 scolon = str_chr(dot, ';'); 506 if (scolon != NULL) { 507 /* Trim version part */ 508 *scolon = '\0'; 509 } 510 511 /* If the extension is an empty string, trim the dot separator. */ 512 if (dot[1] == '\0') 513 *dot = '\0'; 514 } 515 516 return name; 517 } 518 519 static bool cdfs_readdir(cdfs_t *fs, fs_node_t *fs_node) 397 520 { 398 521 cdfs_node_t *node = CDFS_NODE(fs_node); … … 408 531 for (uint32_t i = 0; i < blocks; i++) { 409 532 block_t *block; 410 int rc = block_get(&block, service_id, node->lba + i, BLOCK_FLAGS_NONE);533 int rc = block_get(&block, fs->service_id, node->lba + i, BLOCK_FLAGS_NONE); 411 534 if (rc != EOK) 412 535 return false; 413 536 414 cdfs_dir_t *dir = (cdfs_dir_t *) block->data; 415 416 // FIXME: skip '.' and '..' 417 418 for (size_t offset = 0; 419 (dir->length != 0) && (offset < BLOCK_SIZE); 537 cdfs_dir_t *dir; 538 539 for (size_t offset = 0; offset < BLOCK_SIZE; 420 540 offset += dir->length) { 421 541 dir = (cdfs_dir_t *) (block->data + offset); 542 if (dir->length == 0) 543 break; 544 if (offset + dir->length > BLOCK_SIZE) { 545 /* XXX Incorrect FS structure */ 546 break; 547 } 422 548 423 549 cdfs_dentry_type_t dentry_type; … … 427 553 dentry_type = CDFS_FILE; 428 554 555 /* Skip special entries */ 556 557 if (dir->name_length == 1 && 558 dir->name[0] == CDFS_NAME_CURDIR) 559 continue; 560 if (dir->name_length == 1 && 561 dir->name[0] == CDFS_NAME_PARENTDIR) 562 continue; 563 429 564 // FIXME: hack - indexing by dentry byte offset on disc 430 565 431 566 fs_node_t *fn; 432 int rc = create_node(&fn, service_id, dentry_type,567 int rc = create_node(&fn, fs, dentry_type, 433 568 (node->lba + i) * BLOCK_SIZE + offset); 434 569 if ((rc != EOK) || (fn == NULL)) … … 439 574 cur->size = uint32_lb(dir->size); 440 575 441 char *name = (char *) malloc(dir->name_length + 1); 576 char *name = cdfs_decode_name(dir->name, 577 dir->name_length, node->fs->enc, dentry_type); 442 578 if (name == NULL) 443 579 return false; 444 445 memcpy(name, dir->name, dir->name_length);446 name[dir->name_length] = 0;447 580 448 581 // FIXME: check return value … … 462 595 } 463 596 464 static fs_node_t *get_uncached_node( service_id_t service_id, fs_index_t index)597 static fs_node_t *get_uncached_node(cdfs_t *fs, fs_index_t index) 465 598 { 466 599 cdfs_lba_t lba = index / BLOCK_SIZE; … … 468 601 469 602 block_t *block; 470 int rc = block_get(&block, service_id, lba, BLOCK_FLAGS_NONE);603 int rc = block_get(&block, fs->service_id, lba, BLOCK_FLAGS_NONE); 471 604 if (rc != EOK) 472 605 return NULL; … … 481 614 482 615 fs_node_t *fn; 483 rc = create_node(&fn, service_id, dentry_type, index);616 rc = create_node(&fn, fs, dentry_type, index); 484 617 if ((rc != EOK) || (fn == NULL)) 485 618 return NULL; … … 497 630 } 498 631 499 static fs_node_t *get_cached_node( service_id_t service_id, fs_index_t index)632 static fs_node_t *get_cached_node(cdfs_t *fs, fs_index_t index) 500 633 { 501 634 ht_key_t key = { 502 635 .index = index, 503 .service_id = service_id636 .service_id = fs->service_id 504 637 }; 505 638 … … 511 644 } 512 645 513 return get_uncached_node( service_id, index);646 return get_uncached_node(fs, index); 514 647 } 515 648 … … 519 652 520 653 if (!parent->processed) { 521 int rc = cdfs_readdir(parent-> service_id, pfn);654 int rc = cdfs_readdir(parent->fs, pfn); 522 655 if (rc != EOK) 523 656 return rc; 524 657 } 525 658 526 list_foreach(parent->cs_list, link) { 527 cdfs_dentry_t *dentry = 528 list_get_instance(link, cdfs_dentry_t, link); 529 659 list_foreach(parent->cs_list, link, cdfs_dentry_t, dentry) { 530 660 if (str_cmp(dentry->name, component) == 0) { 531 *fn = get_cached_node(parent-> service_id, dentry->index);661 *fn = get_cached_node(parent->fs, dentry->index); 532 662 return EOK; 533 663 } … … 543 673 544 674 if (!node->processed) 545 cdfs_readdir(node-> service_id, fn);675 cdfs_readdir(node->fs, fn); 546 676 547 677 node->opened++; … … 584 714 585 715 if ((node->type == CDFS_DIRECTORY) && (!node->processed)) 586 cdfs_readdir(node-> service_id, fn);716 cdfs_readdir(node->fs, fn); 587 717 588 718 *has_children = !list_empty(&node->cs_list); … … 623 753 { 624 754 return 0; 755 } 756 757 static int cdfs_size_block(service_id_t service_id, uint32_t *size) 758 { 759 *size = BLOCK_SIZE; 760 761 return EOK; 762 } 763 764 static int cdfs_total_block_count(service_id_t service_id, uint64_t *count) 765 { 766 *count = 0; 767 768 return EOK; 769 } 770 771 static int cdfs_free_block_count(service_id_t service_id, uint64_t *count) 772 { 773 *count = 0; 774 775 return EOK; 625 776 } 626 777 … … 641 792 .is_directory = cdfs_is_directory, 642 793 .is_file = cdfs_is_file, 643 .service_get = cdfs_service_get 794 .service_get = cdfs_service_get, 795 .size_block = cdfs_size_block, 796 .total_block_count = cdfs_total_block_count, 797 .free_block_count = cdfs_free_block_count 644 798 }; 645 799 646 static bool iso_readfs(service_id_t service_id, fs_node_t *rfn, 800 /** Verify that escape sequence corresonds to one of the allowed encoding 801 * escape sequences allowed for Joliet. */ 802 static int cdfs_verify_joliet_esc_seq(uint8_t *seq) 803 { 804 size_t i, j, k; 805 bool match; 806 807 i = 0; 808 while (i + ucs2_esc_seq_len <= 32) { 809 if (seq[i] == 0) 810 break; 811 812 for (j = 0; j < ucs2_esc_seq_no; j++) { 813 match = true; 814 for (k = 0; k < ucs2_esc_seq_len; k++) 815 if (seq[i + k] != ucs2_esc_seq[j][k]) 816 match = false; 817 if (match) { 818 break; 819 } 820 } 821 822 if (!match) 823 return EINVAL; 824 825 i += ucs2_esc_seq_len; 826 } 827 828 while (i < 32) { 829 if (seq[i] != 0) 830 return EINVAL; 831 ++i; 832 } 833 834 return EOK; 835 } 836 837 /** Find Joliet supplementary volume descriptor. 838 * 839 * @param sid Block device service ID 840 * @param altroot First filesystem block 841 * @param rlba Place to store LBA of root dir 842 * @param rsize Place to store size of root dir 843 * @return EOK if found, ENOENT if not 844 */ 845 static int cdfs_find_joliet_svd(service_id_t sid, cdfs_lba_t altroot, 846 uint32_t *rlba, uint32_t *rsize) 847 { 848 cdfs_lba_t bi; 849 850 for (bi = altroot + 17; ; bi++) { 851 block_t *block; 852 int rc = block_get(&block, sid, bi, BLOCK_FLAGS_NONE); 853 if (rc != EOK) 854 break; 855 856 cdfs_vol_desc_t *vol_desc = (cdfs_vol_desc_t *) block->data; 857 858 if (vol_desc->type == VOL_DESC_SET_TERMINATOR) { 859 block_put(block); 860 break; 861 } 862 863 if ((vol_desc->type != VOL_DESC_SUPPLEMENTARY) || 864 (memcmp(vol_desc->standard_ident, CDFS_STANDARD_IDENT, 5) != 0) || 865 (vol_desc->version != 1)) { 866 block_put(block); 867 continue; 868 } 869 870 uint16_t set_size = uint16_lb(vol_desc->data.prisec.set_size); 871 if (set_size > 1) { 872 /* 873 * Technically, we don't support multi-disc sets. 874 * But one can encounter erroneously mastered 875 * images in the wild and it might actually work 876 * for the first disc in the set. 877 */ 878 } 879 880 uint16_t sequence_nr = uint16_lb(vol_desc->data.prisec.sequence_nr); 881 if (sequence_nr != 1) { 882 /* 883 * We only support the first disc 884 * in multi-disc sets. 885 */ 886 block_put(block); 887 continue; 888 } 889 890 uint16_t block_size = uint16_lb(vol_desc->data.prisec.block_size); 891 if (block_size != BLOCK_SIZE) { 892 block_put(block); 893 continue; 894 } 895 896 rc = cdfs_verify_joliet_esc_seq(vol_desc->data.prisec.esc_seq); 897 if (rc != EOK) 898 continue; 899 *rlba = uint32_lb(vol_desc->data.prisec.root_dir.lba); 900 *rsize = uint32_lb(vol_desc->data.prisec.root_dir.size); 901 block_put(block); 902 return EOK; 903 } 904 905 return ENOENT; 906 } 907 908 static bool iso_readfs(cdfs_t *fs, fs_node_t *rfn, 647 909 cdfs_lba_t altroot) 648 910 { 649 911 /* First 16 blocks of isofs are empty */ 650 912 block_t *block; 651 int rc = block_get(&block, service_id, altroot + 16, BLOCK_FLAGS_NONE);913 int rc = block_get(&block, fs->service_id, altroot + 16, BLOCK_FLAGS_NONE); 652 914 if (rc != EOK) 653 915 return false; … … 666 928 } 667 929 668 uint16_t set_size = uint16_lb(vol_desc->data.pri mary.set_size);930 uint16_t set_size = uint16_lb(vol_desc->data.prisec.set_size); 669 931 if (set_size > 1) { 670 932 /* … … 676 938 } 677 939 678 uint16_t sequence_nr = uint16_lb(vol_desc->data.pri mary.sequence_nr);940 uint16_t sequence_nr = uint16_lb(vol_desc->data.prisec.sequence_nr); 679 941 if (sequence_nr != 1) { 680 942 /* … … 686 948 } 687 949 688 uint16_t block_size = uint16_lb(vol_desc->data.pri mary.block_size);950 uint16_t block_size = uint16_lb(vol_desc->data.prisec.block_size); 689 951 if (block_size != BLOCK_SIZE) { 690 952 block_put(block); … … 695 957 696 958 cdfs_node_t *node = CDFS_NODE(rfn); 697 node->lba = uint32_lb(vol_desc->data.primary.root_dir.lba); 698 node->size = uint32_lb(vol_desc->data.primary.root_dir.size); 699 700 if (!cdfs_readdir(service_id, rfn)) { 959 960 /* Search for Joliet SVD */ 961 962 uint32_t jrlba; 963 uint32_t jrsize; 964 965 rc = cdfs_find_joliet_svd(fs->service_id, altroot, &jrlba, &jrsize); 966 if (rc == EOK) { 967 /* Found */ 968 node->lba = jrlba; 969 node->size = jrsize; 970 fs->enc = enc_ucs2; 971 } else { 972 node->lba = uint32_lb(vol_desc->data.prisec.root_dir.lba); 973 node->size = uint32_lb(vol_desc->data.prisec.root_dir.size); 974 fs->enc = enc_ascii; 975 } 976 977 if (!cdfs_readdir(fs, rfn)) { 701 978 block_put(block); 702 979 return false; … … 710 987 * 711 988 */ 712 static bool cdfs_instance_init(service_id_t service_id, cdfs_lba_t altroot) 713 { 989 static cdfs_t *cdfs_fs_create(service_id_t sid, cdfs_lba_t altroot) 990 { 991 cdfs_t *fs = NULL; 992 fs_node_t *rfn = NULL; 993 994 fs = calloc(1, sizeof(cdfs_t)); 995 if (fs == NULL) 996 goto error; 997 998 fs->service_id = sid; 999 714 1000 /* Create root node */ 715 fs_node_t *rfn; 716 int rc = create_node(&rfn, service_id, L_DIRECTORY, cdfs_index++); 1001 int rc = create_node(&rfn, fs, L_DIRECTORY, cdfs_index++); 717 1002 718 1003 if ((rc != EOK) || (!rfn)) 719 return false;1004 goto error; 720 1005 721 1006 /* FS root is not linked */ … … 725 1010 726 1011 /* Check if there is cdfs in given session */ 727 if (!iso_readfs(service_id, rfn, altroot)) { 728 // XXX destroy node 729 return false; 730 } 731 732 return true; 1012 if (!iso_readfs(fs, rfn, altroot)) 1013 goto error; 1014 1015 list_append(&fs->link, &cdfs_instances); 1016 return fs; 1017 error: 1018 // XXX destroy node 1019 free(fs); 1020 return NULL; 733 1021 } 734 1022 … … 737 1025 { 738 1026 /* Initialize the block layer */ 739 int rc = block_init( EXCHANGE_SERIALIZE,service_id, BLOCK_SIZE);1027 int rc = block_init(service_id, BLOCK_SIZE); 740 1028 if (rc != EOK) 741 1029 return rc; … … 748 1036 altroot = 0; 749 1037 } else { 750 /* Read TOC and find the last session */ 751 toc_block_t *toc = block_get_toc(service_id, 1); 752 if ((toc != NULL) && (uint16_t_be2host(toc->size) == 10)) { 753 altroot = uint32_t_be2host(toc->first_lba); 754 free(toc); 755 } 1038 /* 1039 * Read TOC multisession information and get the start address 1040 * of the first track in the last session 1041 */ 1042 scsi_toc_multisess_data_t toc; 1043 1044 rc = block_read_toc(service_id, 1, &toc, sizeof(toc)); 1045 if (rc == EOK && (uint16_t_be2host(toc.toc_len) == 10)) 1046 altroot = uint32_t_be2host(toc.ftrack_lsess.start_addr); 756 1047 } 757 1048 … … 774 1065 } 775 1066 776 /* Initialize cdfs instance */777 if ( !cdfs_instance_init(service_id, altroot)) {1067 /* Create cdfs instance */ 1068 if (cdfs_fs_create(service_id, altroot) == NULL) { 778 1069 block_cache_fini(service_id); 779 1070 block_fini(service_id); … … 798 1089 cdfs_node_t *node = hash_table_get_inst(item, cdfs_node_t, nh_link); 799 1090 800 if (node-> service_id == service_id) {1091 if (node->fs->service_id == service_id) { 801 1092 hash_table_remove_item(&nodes, &node->nh_link); 802 1093 } … … 805 1096 } 806 1097 807 static void cdfs_instance_done(service_id_t service_id) 808 { 809 hash_table_apply(&nodes, rm_service_id_nodes, &service_id); 810 block_cache_fini(service_id); 811 block_fini(service_id); 1098 static void cdfs_fs_destroy(cdfs_t *fs) 1099 { 1100 list_remove(&fs->link); 1101 hash_table_apply(&nodes, rm_service_id_nodes, &fs->service_id); 1102 block_cache_fini(fs->service_id); 1103 block_fini(fs->service_id); 1104 free(fs); 1105 } 1106 1107 static cdfs_t *cdfs_find_by_sid(service_id_t service_id) 1108 { 1109 list_foreach(cdfs_instances, link, cdfs_t, fs) { 1110 if (fs->service_id == service_id) 1111 return fs; 1112 } 1113 1114 return NULL; 812 1115 } 813 1116 814 1117 static int cdfs_unmounted(service_id_t service_id) 815 1118 { 816 cdfs_instance_done(service_id); 1119 cdfs_t *fs; 1120 1121 fs = cdfs_find_by_sid(service_id); 1122 if (fs == NULL) 1123 return ENOENT; 1124 1125 cdfs_fs_destroy(fs); 817 1126 return EOK; 818 1127 } … … 834 1143 835 1144 if (!node->processed) { 836 int rc = cdfs_readdir( service_id, FS_NODE(node));1145 int rc = cdfs_readdir(node->fs, FS_NODE(node)); 837 1146 if (rc != EOK) 838 1147 return rc; … … 917 1226 /* Some nodes were requested to be removed from the cache. */ 918 1227 if (0 < *premove_cnt) { 919 cdfs_node_t *node = 1228 cdfs_node_t *node = hash_table_get_inst(item, cdfs_node_t, nh_link); 920 1229 921 1230 if (!node->opened) {
Note:
See TracChangeset
for help on using the changeset viewer.