1/* 2 * Copyright 1999, Be Incorporated. All Rights Reserved. 3 * This file may be used under the terms of the Be Sample Code License. 4 * 5 * Copyright 2001, pinc Software. All Rights Reserved. 6 * 7 * iso9960/multi-session, 1.0.0 8 */ 9 10 11#include <ctype.h> 12 13#ifndef FS_SHELL 14# include <dirent.h> 15# include <errno.h> 16# include <fcntl.h> 17# include <stdio.h> 18# include <stdlib.h> 19# include <string.h> 20# include <sys/stat.h> 21# include <time.h> 22# include <unistd.h> 23 24# include <KernelExport.h> 25# include <NodeMonitor.h> 26# include <fs_interface.h> 27# include <fs_cache.h> 28 29# include <fs_attr.h> 30# include <fs_info.h> 31# include <fs_index.h> 32# include <fs_query.h> 33# include <fs_volume.h> 34 35# include <util/kernel_cpp.h> 36#endif 37 38#include "iso9660.h" 39#include "iso9660_identify.h" 40 41// TODO: temporary solution as long as there is no public I/O requests API 42#include <io_requests.h> 43 44 45//#define TRACE_ISO9660 46#ifdef TRACE_ISO9660 47# define TRACE(x) dprintf x 48#else 49# define TRACE(x) ; 50#endif 51 52 53struct identify_cookie { 54 iso9660_info info; 55}; 56 57extern fs_volume_ops gISO9660VolumeOps; 58extern fs_vnode_ops gISO9660VnodeOps; 59 60 61//! fs_io() callback hook 62static status_t 63iterative_io_get_vecs_hook(void* cookie, io_request* request, off_t offset, 64 size_t size, struct file_io_vec* vecs, size_t* _count) 65{ 66 iso9660_inode* node = (iso9660_inode*)cookie; 67 68 vecs->offset = offset + ((off_t)node->startLBN[FS_DATA_FORMAT] 69 * (off_t)node->volume->logicalBlkSize[FS_DATA_FORMAT]); 70 vecs->length = size; 71 72 *_count = 1; 73 74 return B_OK; 75} 76 77 78//! fs_io() callback hook 79static status_t 80iterative_io_finished_hook(void* cookie, io_request* request, status_t status, 81 bool partialTransfer, size_t bytesTransferred) 82{ 83 // nothing to do here... 84 return B_OK; 85} 86 87 88// #pragma mark - Scanning 89 90 91static float 92fs_identify_partition(int fd, partition_data* partition, void** _cookie) 93{ 94 iso9660_info* info = new iso9660_info; 95 96 status_t status = iso9660_fs_identify(fd, info); 97 if (status != B_OK) { 98 delete info; 99 return -1; 100 } 101 102 *_cookie = info; 103 return 0.6f; 104} 105 106 107static status_t 108fs_scan_partition(int fd, partition_data* partition, void* _cookie) 109{ 110 iso9660_info* info = (iso9660_info*)_cookie; 111 112 partition->status = B_PARTITION_VALID; 113 partition->flags |= B_PARTITION_FILE_SYSTEM | B_PARTITION_READ_ONLY ; 114 partition->block_size = ISO_PVD_SIZE; 115 partition->content_size = ISO_PVD_SIZE * info->max_blocks; 116 partition->content_name = strdup(info->PreferredName()); 117 118 if (partition->content_name == NULL) 119 return B_NO_MEMORY; 120 121 return B_OK; 122} 123 124 125static void 126fs_free_identify_partition_cookie(partition_data* partition, void* _cookie) 127{ 128 delete (iso9660_info*)_cookie; 129} 130 131 132// #pragma mark - FS hooks 133 134 135static status_t 136fs_mount(fs_volume* _volume, const char* device, uint32 flags, 137 const char* args, ino_t* _rootID) 138{ 139 bool allowJoliet = true; 140 iso9660_volume* volume; 141 142 // Check for a 'nojoliet' parm 143 // all we check for is the existance of 'nojoliet' in the parms. 144 if (args != NULL) { 145 uint32 i; 146 char* spot; 147 char* buf = strdup(args); 148 149 uint32 len = strlen(buf); 150 // lower case the parms data 151 for (i = 0; i < len + 1; i++) 152 buf[i] = tolower(buf[i]); 153 154 // look for nojoliet 155 spot = strstr(buf, "nojoliet"); 156 if (spot != NULL) 157 allowJoliet = false; 158 159 free(buf); 160 } 161 162 // Try and mount volume as an ISO volume. 163 status_t result = ISOMount(device, O_RDONLY, &volume, allowJoliet); 164 if (result == B_OK) { 165 *_rootID = ISO_ROOTNODE_ID; 166 167 _volume->private_volume = volume; 168 _volume->ops = &gISO9660VolumeOps; 169 volume->volume = _volume; 170 volume->id = _volume->id; 171 172 result = publish_vnode(_volume, *_rootID, &volume->rootDirRec, 173 &gISO9660VnodeOps, 174 volume->rootDirRec.attr.stat[FS_DATA_FORMAT].st_mode, 0); 175 if (result != B_OK) { 176 block_cache_delete(volume->fBlockCache, false); 177 free(volume); 178 result = B_ERROR; 179 } 180 } 181 return result; 182} 183 184 185static status_t 186fs_unmount(fs_volume* _volume) 187{ 188 status_t result = B_NO_ERROR; 189 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 190 191 TRACE(("fs_unmount - ENTER\n")); 192 193 // Unlike in BeOS, we need to put the reference to our root node ourselves 194 put_vnode(_volume, ISO_ROOTNODE_ID); 195 196 block_cache_delete(volume->fBlockCache, false); 197 close(volume->fdOfSession); 198 result = close(volume->fd); 199 200 free(volume); 201 202 TRACE(("fs_unmount - EXIT, result is %s\n", strerror(result))); 203 return result; 204} 205 206 207static status_t 208fs_read_fs_stat(fs_volume* _volume, struct fs_info* info) 209{ 210 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 211 212 info->flags = B_FS_IS_PERSISTENT | B_FS_IS_READONLY; 213 info->block_size = volume->logicalBlkSize[FS_DATA_FORMAT]; 214 info->io_size = 65536; 215 info->total_blocks = volume->volSpaceSize[FS_DATA_FORMAT]; 216 info->free_blocks = 0; 217 218 strlcpy(info->device_name, volume->devicePath, sizeof(info->device_name)); 219 strlcpy(info->volume_name, volume->volIDString, sizeof(info->volume_name)); 220 221 // strip trailing spaces 222 int i; 223 for (i = strlen(info->volume_name) - 1; i >=0 ; i--) { 224 if (info->volume_name[i] != ' ') 225 break; 226 } 227 228 if (i < 0) 229 strcpy(info->volume_name, "UNKNOWN"); 230 else 231 info->volume_name[i + 1] = 0; 232 233 strcpy(info->fsh_name, "iso9660"); 234 return B_OK; 235} 236 237 238static status_t 239fs_get_vnode_name(fs_volume* _volume, fs_vnode* _node, char* buffer, 240 size_t bufferSize) 241{ 242 iso9660_inode* node = (iso9660_inode*)_node->private_node; 243 244 strlcpy(buffer, node->name, bufferSize); 245 return B_OK; 246} 247 248 249static status_t 250fs_walk(fs_volume* _volume, fs_vnode* _base, const char* file, ino_t* _vnodeID) 251{ 252 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 253 iso9660_inode* baseNode = (iso9660_inode*)_base->private_node; 254 iso9660_inode* newNode = NULL; 255 256 TRACE(("fs_walk - looking for %s in dir file of length %d\n", file, 257 (int)baseNode->dataLen[FS_DATA_FORMAT])); 258 259 if (strcmp(file, ".") == 0) { 260 // base directory 261 TRACE(("fs_walk - found \".\" file.\n")); 262 *_vnodeID = baseNode->id; 263 return get_vnode(_volume, *_vnodeID, NULL); 264 } else if (strcmp(file, "..") == 0) { 265 // parent directory 266 TRACE(("fs_walk - found \"..\" file.\n")); 267 *_vnodeID = baseNode->parID; 268 return get_vnode(_volume, *_vnodeID, NULL); 269 } 270 271 // look up file in the directory 272 uint32 dataLength = baseNode->dataLen[FS_DATA_FORMAT]; 273 status_t result = ENOENT; 274 size_t totalRead = 0; 275 off_t block = baseNode->startLBN[FS_DATA_FORMAT]; 276 bool done = false; 277 278 while (totalRead < dataLength && !done) { 279 off_t cachedBlock = block; 280 char* blockData = (char*)block_cache_get(volume->fBlockCache, block); 281 if (blockData == NULL) 282 break; 283 284 size_t bytesRead = 0; 285 off_t blockBytesRead = 0; 286 iso9660_inode node; 287 int initResult; 288 289 TRACE(("fs_walk - read buffer from disk at LBN %Ld into buffer " 290 "%p.\n", block, blockData)); 291 292 // Move to the next block if necessary 293 // Don't go over end of buffer, if dir record sits on boundary. 294 295 while (blockBytesRead < volume->logicalBlkSize[FS_DATA_FORMAT] 296 && totalRead + blockBytesRead < dataLength 297 && blockData[0] != 0 298 && !done) { 299 initResult = InitNode(volume, &node, blockData, &bytesRead); 300 TRACE(("fs_walk - InitNode returned %s, filename %s, %u bytes " 301 "read\n", strerror(initResult), node.name, (unsigned)bytesRead)); 302 303 if (initResult == B_OK) { 304 if ((node.flags & ISO_IS_ASSOCIATED_FILE) == 0 305 && !strcmp(node.name, file)) { 306 TRACE(("fs_walk - success, found vnode at block %Ld, pos " 307 "%Ld\n", block, blockBytesRead)); 308 309 *_vnodeID = (block << 30) + (blockBytesRead & 0xffffffff); 310 TRACE(("fs_walk - New vnode id is %Ld\n", *_vnodeID)); 311 312 result = get_vnode(_volume, *_vnodeID, (void**)&newNode); 313 if (result == B_OK) { 314 newNode->parID = baseNode->id; 315 done = true; 316 } 317 } else { 318 free(node.name); 319 free(node.attr.slName); 320 } 321 } else { 322 result = initResult; 323 if (bytesRead == 0) 324 done = true; 325 } 326 blockData += bytesRead; 327 blockBytesRead += bytesRead; 328 329 TRACE(("fs_walk - Adding %u bytes to blockBytes read (total " 330 "%Ld/%u).\n", (unsigned)bytesRead, blockBytesRead, 331 (unsigned)baseNode->dataLen[FS_DATA_FORMAT])); 332 } 333 totalRead += volume->logicalBlkSize[FS_DATA_FORMAT]; 334 block++; 335 336 TRACE(("fs_walk - moving to next block %Ld, total read %u\n", 337 block, (unsigned)totalRead)); 338 block_cache_put(volume->fBlockCache, cachedBlock); 339 } 340 341 TRACE(("fs_walk - EXIT, result is %s, vnid is %Lu\n", 342 strerror(result), *_vnodeID)); 343 return result; 344} 345 346 347static status_t 348fs_read_vnode(fs_volume* _volume, ino_t vnodeID, fs_vnode* _node, 349 int* _type, uint32* _flags, bool reenter) 350{ 351 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 352 353 iso9660_inode* newNode = (iso9660_inode*)calloc(sizeof(iso9660_inode), 1); 354 if (newNode == NULL) 355 return B_NO_MEMORY; 356 357 uint32 pos = vnodeID & 0x3fffffff; 358 uint32 block = vnodeID >> 30; 359 360 TRACE(("fs_read_vnode - block = %u, pos = %u, raw = %Lu node %p\n", 361 (unsigned)block, (unsigned) pos, vnodeID, newNode)); 362 363 if (pos > volume->logicalBlkSize[FS_DATA_FORMAT]) { 364 free(newNode); 365 return B_BAD_VALUE; 366 } 367 368 char* data = (char*)block_cache_get(volume->fBlockCache, block); 369 if (data == NULL) { 370 free(newNode); 371 return B_IO_ERROR; 372 } 373 374 status_t result = InitNode(volume, newNode, data + pos, NULL); 375 block_cache_put(volume->fBlockCache, block); 376 377 if (result < B_OK) { 378 free(newNode); 379 return result; 380 } 381 382 newNode->volume = volume; 383 newNode->id = vnodeID; 384 385 _node->private_node = newNode; 386 _node->ops = &gISO9660VnodeOps; 387 *_type = newNode->attr.stat[FS_DATA_FORMAT].st_mode 388 & ~(S_IWUSR | S_IWGRP | S_IWOTH); 389 *_flags = 0; 390 391 if ((newNode->flags & ISO_IS_DIR) == 0) { 392 newNode->cache = file_cache_create(volume->id, vnodeID, 393 newNode->dataLen[FS_DATA_FORMAT]); 394 } 395 396 return B_OK; 397} 398 399 400static status_t 401fs_release_vnode(fs_volume* /*_volume*/, fs_vnode* _node, bool /*reenter*/) 402{ 403 iso9660_inode* node = (iso9660_inode*)_node->private_node; 404 405 TRACE(("fs_release_vnode - ENTER (%p)\n", node)); 406 407 if (node->id != ISO_ROOTNODE_ID) { 408 free(node->name); 409 free(node->attr.slName); 410 411 if (node->cache != NULL) 412 file_cache_delete(node->cache); 413 414 free(node); 415 } 416 417 TRACE(("fs_release_vnode - EXIT\n")); 418 return B_OK; 419} 420 421 422static status_t 423fs_read_pages(fs_volume* _volume, fs_vnode* _node, void* _cookie, off_t pos, 424 const iovec* vecs, size_t count, size_t* _numBytes) 425{ 426 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 427 iso9660_inode* node = (iso9660_inode*)_node->private_node; 428 429 uint32 fileSize = node->dataLen[FS_DATA_FORMAT]; 430 size_t bytesLeft = *_numBytes; 431 432 if (pos >= fileSize) { 433 *_numBytes = 0; 434 return B_OK; 435 } 436 if (pos + bytesLeft > fileSize) { 437 bytesLeft = fileSize - pos; 438 *_numBytes = bytesLeft; 439 } 440 441 file_io_vec fileVec; 442 fileVec.offset = pos + ((off_t)node->startLBN[FS_DATA_FORMAT] 443 * (off_t)volume->logicalBlkSize[FS_DATA_FORMAT]); 444 fileVec.length = bytesLeft; 445 446 uint32 vecIndex = 0; 447 size_t vecOffset = 0; 448 return read_file_io_vec_pages(volume->fd, &fileVec, 1, vecs, count, 449 &vecIndex, &vecOffset, &bytesLeft); 450} 451 452 453static status_t 454fs_io(fs_volume* _volume, fs_vnode* _node, void* _cookie, io_request* request) 455{ 456 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 457 iso9660_inode* node = (iso9660_inode*)_node->private_node; 458 459 if (io_request_is_write(request)) { 460 notify_io_request(request, B_READ_ONLY_DEVICE); 461 return B_READ_ONLY_DEVICE; 462 } 463 464 if ((node->flags & ISO_IS_DIR) != 0) { 465 notify_io_request(request, B_IS_A_DIRECTORY); 466 return B_IS_A_DIRECTORY; 467 } 468 469 return do_iterative_fd_io(volume->fd, request, iterative_io_get_vecs_hook, 470 iterative_io_finished_hook, node); 471} 472 473 474static status_t 475fs_read_stat(fs_volume* _volume, fs_vnode* _node, struct stat* st) 476{ 477 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 478 iso9660_inode* node = (iso9660_inode*)_node->private_node; 479 status_t result = B_NO_ERROR; 480 time_t time; 481 482 TRACE(("fs_read_stat - ENTER\n")); 483 484 st->st_dev = volume->id; 485 st->st_ino = node->id; 486 st->st_nlink = node->attr.stat[FS_DATA_FORMAT].st_nlink; 487 st->st_uid = node->attr.stat[FS_DATA_FORMAT].st_uid; 488 st->st_gid = node->attr.stat[FS_DATA_FORMAT].st_gid; 489 st->st_blksize = 65536; 490 st->st_mode = node->attr.stat[FS_DATA_FORMAT].st_mode; 491 492 // Same for file/dir in ISO9660 493 st->st_size = node->dataLen[FS_DATA_FORMAT]; 494 st->st_blocks = (st->st_size + 511) / 512; 495 if (ConvertRecDate(&(node->recordDate), &time) == B_NO_ERROR) 496 st->st_ctime = st->st_mtime = st->st_atime = time; 497 498 TRACE(("fs_read_stat - EXIT, result is %s\n", strerror(result))); 499 500 return result; 501} 502 503 504static status_t 505fs_open(fs_volume* /*_volume*/, fs_vnode* _node, int openMode, void** /*cookie*/) 506{ 507 // Do not allow any of the write-like open modes to get by 508 if ((openMode & O_RWMASK) == O_WRONLY || (openMode & O_RWMASK) == O_RDWR 509 || (openMode & O_TRUNC) != 0 || (openMode & O_CREAT) != 0) 510 return EROFS; 511 512 return B_OK; 513} 514 515 516static status_t 517fs_read(fs_volume* _volume, fs_vnode* _node, void* cookie, off_t pos, 518 void* buffer, size_t* _length) 519{ 520 iso9660_inode* node = (iso9660_inode*)_node->private_node; 521 522 if ((node->flags & ISO_IS_DIR) != 0) 523 return EISDIR; 524 525 uint32 fileSize = node->dataLen[FS_DATA_FORMAT]; 526 527 // set/check boundaries for pos/length 528 if (pos < 0) 529 return B_BAD_VALUE; 530 if (pos >= fileSize) { 531 *_length = 0; 532 return B_OK; 533 } 534 535 return file_cache_read(node->cache, NULL, pos, buffer, _length); 536} 537 538 539static status_t 540fs_close(fs_volume* /*_volume*/, fs_vnode* /*_node*/, void* /*cookie*/) 541{ 542 return B_OK; 543} 544 545 546static status_t 547fs_free_cookie(fs_volume* /*_volume*/, fs_vnode* /*_node*/, void* /*cookie*/) 548{ 549 return B_OK; 550} 551 552 553static status_t 554fs_access(fs_volume* /*_volume*/, fs_vnode* /*_node*/, int /*mode*/) 555{ 556 return B_OK; 557} 558 559 560static status_t 561fs_read_link(fs_volume* _volume, fs_vnode* _node, char* buffer, 562 size_t* _bufferSize) 563{ 564 iso9660_inode* node = (iso9660_inode*)_node->private_node; 565 566 if (!S_ISLNK(node->attr.stat[FS_DATA_FORMAT].st_mode)) 567 return B_BAD_VALUE; 568 569 size_t length = strlen(node->attr.slName); 570 if (length > *_bufferSize) 571 memcpy(buffer, node->attr.slName, *_bufferSize); 572 else { 573 memcpy(buffer, node->attr.slName, length); 574 *_bufferSize = length; 575 } 576 577 return B_OK; 578} 579 580 581static status_t 582fs_open_dir(fs_volume* /*_volume*/, fs_vnode* _node, void** _cookie) 583{ 584 iso9660_inode* node = (iso9660_inode*)_node->private_node; 585 586 TRACE(("fs_open_dir - node is %p\n", node)); 587 588 if ((node->flags & ISO_IS_DIR) == 0) 589 return B_NOT_A_DIRECTORY; 590 591 dircookie* dirCookie = (dircookie*)malloc(sizeof(dircookie)); 592 if (dirCookie == NULL) 593 return B_NO_MEMORY; 594 595 dirCookie->startBlock = node->startLBN[FS_DATA_FORMAT]; 596 dirCookie->block = node->startLBN[FS_DATA_FORMAT]; 597 dirCookie->totalSize = node->dataLen[FS_DATA_FORMAT]; 598 dirCookie->pos = 0; 599 dirCookie->id = node->id; 600 *_cookie = (void*)dirCookie; 601 602 return B_OK; 603} 604 605 606static status_t 607fs_read_dir(fs_volume* _volume, fs_vnode* _node, void* _cookie, 608 struct dirent* buffer, size_t bufferSize, uint32* num) 609{ 610 iso9660_volume* volume = (iso9660_volume*)_volume->private_volume; 611 dircookie* dirCookie = (dircookie*)_cookie; 612 613 TRACE(("fs_read_dir - ENTER\n")); 614 615 status_t result = ISOReadDirEnt(volume, dirCookie, buffer, bufferSize); 616 617 // If we succeeded, return 1, the number of dirents we read. 618 if (result == B_OK) 619 *num = 1; 620 else 621 *num = 0; 622 623 // When you get to the end, don't return an error, just return 624 // a zero in *num. 625 626 if (result == B_ENTRY_NOT_FOUND) 627 result = B_OK; 628 629 TRACE(("fs_read_dir - EXIT, result is %s\n", strerror(result))); 630 return result; 631} 632 633 634static status_t 635fs_rewind_dir(fs_volume* _volume, fs_vnode* _node, void* _cookie) 636{ 637 dircookie* cookie = (dircookie*)_cookie; 638 639 cookie->block = cookie->startBlock; 640 cookie->pos = 0; 641 return B_OK; 642} 643 644 645static status_t 646fs_close_dir(fs_volume* _volume, fs_vnode* _node, void* cookie) 647{ 648 return B_OK; 649} 650 651 652static status_t 653fs_free_dir_cookie(fs_volume* _volume, fs_vnode* _node, void* cookie) 654{ 655 free(cookie); 656 return B_OK; 657} 658 659 660// #pragma mark - 661 662 663static status_t 664iso_std_ops(int32 op, ...) 665{ 666 switch (op) { 667 case B_MODULE_INIT: 668 case B_MODULE_UNINIT: 669 return B_OK; 670 default: 671 return B_ERROR; 672 } 673} 674 675 676fs_volume_ops gISO9660VolumeOps = { 677 &fs_unmount, 678 &fs_read_fs_stat, 679 NULL, 680 NULL, 681 &fs_read_vnode, 682 683 /* index and index directory ops */ 684 NULL, 685 NULL, 686 NULL, 687 NULL, 688 NULL, 689 NULL, 690 NULL, 691 NULL, 692 693 /* query ops */ 694 NULL, 695 NULL, 696 NULL, 697 NULL, 698 NULL, 699 700 /* FS layer ops */ 701 NULL, 702 NULL, 703}; 704 705fs_vnode_ops gISO9660VnodeOps = { 706 &fs_walk, 707 &fs_get_vnode_name, 708 &fs_release_vnode, 709 NULL, 710 711 /* vm-related ops */ 712 NULL, 713 &fs_read_pages, 714 NULL, 715 716 &fs_io, 717 NULL, // cancel_io() 718 719 /* cache file access */ 720 NULL, 721 722 /* common */ 723 NULL, 724 NULL, 725 NULL, 726 NULL, 727 NULL, 728 &fs_read_link, 729 NULL, 730 NULL, 731 NULL, 732 NULL, 733 &fs_access, 734 &fs_read_stat, 735 NULL, 736 NULL, 737 738 /* file */ 739 NULL, 740 &fs_open, 741 &fs_close, 742 &fs_free_cookie, 743 &fs_read, 744 NULL, 745 746 /* dir */ 747 NULL, 748 NULL, 749 &fs_open_dir, 750 &fs_close_dir, 751 &fs_free_dir_cookie, 752 &fs_read_dir, 753 &fs_rewind_dir, 754 755 /* attribute directory ops */ 756 NULL, 757 NULL, 758 NULL, 759 NULL, 760 NULL, 761 762 /* attribute ops */ 763 NULL, 764 NULL, 765 NULL, 766 NULL, 767 NULL, 768 NULL, 769 NULL, 770 NULL, 771 NULL, 772 NULL, 773 774 /* node and FS layer support */ 775 NULL, 776 NULL, 777}; 778 779static file_system_module_info sISO660FileSystem = { 780 { 781 "file_systems/iso9660" B_CURRENT_FS_API_VERSION, 782 0, 783 iso_std_ops, 784 }, 785 786 "iso9660", // short_name 787 "ISO9660 File System", // pretty_name 788 0, // DDM flags 789 790 // scanning 791 fs_identify_partition, 792 fs_scan_partition, 793 fs_free_identify_partition_cookie, 794 NULL, // free_partition_content_cookie() 795 796 &fs_mount, 797 798 /* capability querying */ 799 NULL, 800 NULL, 801 NULL, 802 NULL, 803 NULL, 804 NULL, 805 806 /* shadow partition modifications */ 807 NULL, 808 809 /* writing */ 810 NULL, 811 NULL, 812 NULL, 813 NULL, 814 NULL, 815 NULL, 816 NULL, 817}; 818 819module_info* modules[] = { 820 (module_info*)&sISO660FileSystem, 821 NULL, 822}; 823 824