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