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