diff --git a/parts/blockdev.asm b/parts/blockdev.asm
index 43cee7e..c479497 100644
--- a/parts/blockdev.asm
+++ b/parts/blockdev.asm
@@ -16,6 +16,12 @@
 BLOCKDEV_ERR_OUT_OF_BOUNDS	.equ	0x03
 BLOCKDEV_ERR_UNSUPPORTED	.equ	0x04
 
+BLOCKDEV_SEEK_ABSOLUTE		.equ	0
+BLOCKDEV_SEEK_FORWARD		.equ	1
+BLOCKDEV_SEEK_BACKWARD		.equ	2
+BLOCKDEV_SEEK_BEGINNING		.equ	3
+BLOCKDEV_SEEK_END		.equ	4
+
 ; *** VARIABLES ***
 ; Pointer to the selected block device. A block device is a 8 bytes block of
 ; memory with pointers to GetC, PutC, Seek and Tell routines, in that order.
@@ -133,13 +139,13 @@ blkPutC:
 ; Set position of selected device to the value specified in HL
 blkSeek:
 	push	de
-	cp	1
+	cp	BLOCKDEV_SEEK_FORWARD
 	jr	z, .forward
-	cp	2
+	cp	BLOCKDEV_SEEK_BACKWARD
 	jr	z, .backward
-	cp	3
+	cp	BLOCKDEV_SEEK_BEGINNING
 	jr	z, .beginning
-	cp	4
+	cp	BLOCKDEV_SEEK_END
 	jr	z, .end
 	; all other modes are considered absolute
 	jr	.seek		; for absolute mode, HL is already correct
diff --git a/parts/core.asm b/parts/core.asm
index f32de9a..6127a8b 100644
--- a/parts/core.asm
+++ b/parts/core.asm
@@ -70,15 +70,20 @@ unsetZ:
 
 ; *** STRINGS ***
 
-; Increase HL until the memory address it points to is null for a maximum of
-; 0xff bytes. Returns the new HL value as well as the number of bytes iterated
-; in A.
-findnull:
+; Increase HL until the memory address it points to is equal to A for a maximum
+; of 0xff bytes. Returns the new HL value as well as the number of bytes
+; iterated in A.
+; If a null char is encountered before we find A, processing is stopped in the
+; same way as if we found our char (so, we look for A *or* 0)
+findchar:
 	push	bc
+	ld	c, a	; let's use C as our cp target
 	ld	a, 0xff
 	ld	b, a
 
 .loop:	ld	a, (hl)
+	cp	c
+	jr	z, .end
 	cp	0
 	jr	z, .end
 	inc	hl
diff --git a/parts/fs.asm b/parts/fs.asm
new file mode 100644
index 0000000..49c6b21
--- /dev/null
+++ b/parts/fs.asm
@@ -0,0 +1,294 @@
+; fs
+;
+; Collapse OS filesystem (CFS) is not made to be convenient, but to be simple.
+; This is little more than "named storage blocks". Characteristics:
+;
+; * a filesystem sits upon a blockdev. It needs GetC, PutC, Seek.
+; * No directory. Use filename prefix to group.
+; * First block of each file has metadata. Others are raw data.
+; * No FAT. Files are a chain of blocks of a predefined size. To enumerate
+;   files, you go through metadata blocks.
+; * Fixed allocation. File size is determined at allocation time and cannot be
+;   grown, only shrunk.
+; * New allocations try to find spots to fit in, but go at the end if no spot is
+;   large enough.
+; * Block size is 0x100, max block count per file is 8bit, that means that max
+;   file size: 64k - metadata overhead.
+;
+; *** Selecting a "source" blockdev
+;
+; This unit exposes "fson" shell command to "mount" CFS upon the currently
+; selected device, at the point where its seekptr currently sits. This checks
+; if we have a valid first block and spits an error otherwise.
+;
+; "fson" takes an optional argument which is a number. If non-zero, we don't
+; error out if there's no metadata: we create a new CFS fs with an empty block.
+;
+; The can only be one "mounted" fs at once. Selecting another blockdev through
+; "bsel" foesn't affect the currently mounted fs, which can still be interacted
+; with (which is important if we want to move data around).
+;
+; *** Block metadata
+;
+; At the beginning of the first block of each file, there is this data
+; structure:
+;
+; 3b: Magic number "CFS"
+; 1b: Allocated block count, including the first one. Except for the "ending"
+;     block, this is never zero.
+; 2b: Size of file in bytes (actually written). Little endian.
+; 26b: file name, null terminated. last byte must be null.
+;
+; That gives us 32 bytes of metadata for first first block, leaving a maximum
+; file size of 0xffe0.
+;
+; *** Last block of the chain
+;
+; The last block of the chain is either a block that has no valid block next to
+; it or a block that reports a 0 allocated block count.
+;
+; *** Deleted files
+;
+; When a file is deleted, its name is set to null. This indicates that the
+; allocated space is up for grabs.
+;
+; *** File "handles"
+;
+; Programs will not typically open files themselves. How it works with CFS is
+; that it exposes an API to plug target files in a blockdev ID. This all
+; depends on how you glue parts together, but ideally, you'll have two
+; fs-related blockdev IDs: one for reading, one for writing.
+;
+; Being plugged into the blockdev system, programs will access the files as they
+; would with any other block device.
+;
+; *** Creating a new FS
+;
+; A valid Collapse OS filesystem is nothing more than the 3 bytes 'C', 'F', 'S'
+; next to each other. Placing them at the right place is all you have to do to
+; create your FS.
+
+; *** DEFINES ***
+; Number of handles we want to support
+; FS_HANDLE_COUNT
+; *** CONSTS ***
+FS_MAX_NAME_SIZE	.equ	0x1a
+; Size in bytes of a FS handle:
+; * 2 bytes for starting offset
+; * 2 bytes for file size (we could fetch it from metadata all the time, but it
+;   could be time consuming depending on the underlying device).
+; * 2 bytes for current position.
+; Starting offset is the *metadata* offset. We need, when we write to a handle,
+; to change the size of the file.
+FS_HANDLE_SIZE		.equ	6
+FS_ERR_NO_FS		.equ	0x5
+
+; *** VARIABLES ***
+; A copy of BLOCKDEV_SEL when the FS was mounted. 0 if no FS is mounted.
+FS_BLKSEL	.equ	FS_RAMSTART
+; Offset at which our FS start on mounted device
+FS_START	.equ	FS_BLKSEL+2
+; Offset at which we are currently pointing to with regards to our routines
+; below, which all assume this offset as a context. This offset is not relative
+; to FS_START. It can be used directly with blkSeek.
+FS_PTR		.equ	FS_START+2
+; A place to store tmp data
+FS_TMP		.equ	FS_PTR+2
+FS_HANDLES	.equ	FS_TMP+0x20
+FS_RAMEND	.equ	FS_HANDLES+(FS_HANDLE_COUNT*FS_HANDLE_SIZE)
+
+; *** CODE ***
+
+; *** Navigation ***
+
+; Resets FS_PTR to the beginning. Errors out if no FS is mounted.
+; Sets Z if success, unset if error
+fsBegin:
+	push	hl
+	ld	hl, (FS_START)
+	ld	(FS_PTR), hl
+	pop	hl
+	call	fsPlace
+	call	fsIsValid	; sets Z
+	call	fsPlace
+	ret
+
+; Change current position to the next block with metadata. If it can't (if this
+; is the last valid block), doesn't move.
+; Sets Z according to whether we moved.
+fsNext:
+	call	unsetZ
+	ret
+
+; Make sure that our underlying blockdev is correcly placed.
+; All other routines assumer a properly placed blkdev cursor.
+fsPlace:
+	push	af
+	push	hl
+	xor	a
+	ld	hl, (FS_PTR)
+	call	blkSeek
+	pop	hl
+	pop	af
+	ret
+
+; Create a new file with A blocks allocated to it.
+; Before doing so, enumerate all blocks in search of a deleted file with
+; allocated space big enough. If it does, it will either take the whole space
+; if the allocated space asked is exactly the same, or of it isn't, split the
+; free space in 2 and create a new deleted metadata block next to the newly
+; created block.
+; Places FS_PTR to the newly allocated block. You have to write the new
+; filename yourself.
+fsAlloc:
+	push	hl
+	push	af		; keep A for later
+	call	fsPlace
+	ld	a, BLOCKDEV_SEEK_FORWARD
+	ld	hl, 3
+	call	blkSeek
+	pop	af		; now we want our A arg
+	call	blkPutC
+	pop	hl
+	ret
+
+; *** Metadata ***
+
+; Sets Z according to whether the current block is valid.
+; Don't call other FS routines without checking block validity first: other
+; routines don't do checks.
+fsIsValid:
+	push	hl
+	ld	b, 0x3
+	ld	hl, FS_TMP
+	call	blkRead
+	jr	nz, .error
+	ld	a, 3
+	ld	de, .magic
+	call	strncmp
+	jr	nz, .error
+	; success
+	jr	.end		; Z is set at this point
+.error:
+	call	unsetZ
+.end:
+	pop	hl
+	ret
+.magic:
+	.db "CFS"
+
+; Return, in A, the number of allocated blocks at current position.
+fsAllocatedBlocks:
+	ret
+
+; Return, in HL, the file size at current position.
+fsFileSize:
+	ret
+
+; Return HL, which points to a null-terminated string which contains the
+; filename at current position.
+fsFileName:
+	push	af
+	ld	a, BLOCKDEV_SEEK_FORWARD
+	ld	hl, 6
+	call	blkSeek
+	ld	b, FS_MAX_NAME_SIZE
+	ld	hl, FS_TMP
+	call	blkRead
+	pop	af
+	ret
+
+; *** Handling ***
+
+; Open file at current position into handle at (HL)
+fsOpen:
+	ret
+
+; Ensures that file size in metadata corresponds to file size in handle as (HL).
+fsCommit:
+	ret
+
+; Read a byte in handle at (HL), put it into A and advance the handle's
+; position.
+; Z is set on success, unset if handle is at the end of the file.
+fsRead:
+	ret
+
+; Write byte A in handle at (HL) and advance the handle's position.
+; Z is set on success, unset if handle is at the end of the allocated space.
+fsWrite:
+	ret
+
+; Sets position of handle (HL) to DE. This position does *not* include metadata.
+; It is an offset that starts at actual data.
+; Sets Z if offset is within bounds, unsets Z if it isn't.
+fsSeek:
+	ret
+
+; *** SHELL COMMANDS ***
+; Mount the fs subsystem upon the currently selected blockdev at current offset.
+; Verify is block is valid and error out if its not, mounting nothing.
+; Upon mounting, copy currently selected device in FS_BLKSEL.
+fsOnCmd:
+	.db	"fson", 0, 0, 0
+	push	hl
+	call	blkTell
+	ld	(FS_PTR), hl
+	call	fsPlace
+	call	fsIsValid
+	jr	nz, .error
+	; success
+	ld	(FS_START), hl
+	xor	a
+	jr	.end
+.error:
+	ld	a, FS_ERR_NO_FS
+.end:
+	pop	hl
+	ret
+
+; Lists filenames in currently active FS
+flsCmd:
+	.db	"fls", 0, 0, 0, 0
+	call	fsBegin
+	jr	nz, .error
+	call	fsFileName
+	call	printstr
+	call	printcrlf
+	xor	a
+	jr	.end
+.error:
+	ld	a, FS_ERR_NO_FS
+.end:
+	ret
+
+; Takes one byte block number to allocate as well we one string arg filename
+; and allocates a new file in the current fs.
+fnewCmd:
+	.db	"fnew", 0b001, 0b1001, 0b001
+	push	hl
+	push	de
+	ld	a, (hl)
+	ex	de, hl
+	inc	de
+	call	intoDE
+	ex	de, hl
+	push	hl	; save the filename for later
+	call	fsAlloc
+	call	fsPlace
+	ld	a, BLOCKDEV_SEEK_FORWARD
+	ld	hl, 6
+	call	blkSeek
+	pop	hl	; now we need the filename
+.loop:
+	ld	a, (hl)
+	cp	0
+	jr	z, .end
+	call	blkPutC
+	inc	hl
+	jr	.loop
+.end:
+	pop	de
+	pop	hl
+	xor	a
+	ret
diff --git a/parts/shell.asm b/parts/shell.asm
index f3e3250..20f479c 100644
--- a/parts/shell.asm
+++ b/parts/shell.asm
@@ -91,7 +91,8 @@ shellLoop:
 	; save char for later
 	ex	af, af'
 	ld	hl, SHELL_BUF
-	call	findnull	; HL points to where we need to write
+	xor	a		; look for null
+	call	findchar	; HL points to where we need to write
 				; A is the number of chars in the buf
 	cp	SHELL_BUFSIZE
 	jr	z, .do		; A == bufsize? then our buffer is full. do!
@@ -157,8 +158,8 @@ shellParse:
 	call	intoDE		; Jump from the table entry to the cmd addr.
 
 	; advance the HL pointer to the beginning of the args.
-	ld	a, 4
-	call	addHL
+	ld	a, ' '
+	call	findchar
 
 	; Now, let's have DE point to the argspecs
 	ld	a, 4