Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple File System

0.00/5 (No votes)
2 Jul 2015 1  
Implementing a simple file system as a file.

Introduction

Today we will look at a specification for a simple file system and then at an implementation in C#.

Background

A little while ago I was writing a program that is able use a file as a file system. I was looking around the internet for a file system that was suitable for my needs. These two links are good sources for those who wants to look into many different file systems. https://en.wikipedia.org/wiki/List_of_file_systems and http://wiki.osdev.org/File_Systems While looking I became curious about how simple file systems could get. I looked for some, but my google fu was not strong enough. So I decided to roll my own file system. You can find the specification in the download section.

Read Only File System

The file system I wrote is called Rofs or Read only file system. It is dead simple to use. Here is an overview of a disk with Rofs on it.

The system allows for easy reading, but because of its simplicity it has no attributes, fault tolerance etc. For specific information, look at the pdf which you find in the download section.

Let's Code!

I have made an extraction of my implementation from the original project which is a console program that can work with multiple file systems. The class we are interested in is the Rofs class. Let's take a look at its methods.

/// <summary>
/// Initializes a new instance of the <see cref="Rofs"/> class.
/// </summary>
/// <param name="stream">Stream.</param>
public Rofs(Stream stream) : base(stream)
{
    if (!IsRofs(stream))
        throw new ArgumentException("The stream doesn't contain a rofs.", "stream");
    
    fileEntries = ReadStructure<ushort>(516);
}

The constructor is simple. Check if the stream contains a Rofs and figure out the number of file that are on it.

/// <summary>
/// Determines if the <paramref name="disk"/> contains a rofs.
/// </summary>
/// <param name="disk">The stream that represents a hard disk.</param>
/// <returns>True if the disk contains a rofs.</returns>
public static bool IsRofs(Stream disk)
{
    disk.Seek(0x0200, SeekOrigin.Begin);
    byte[] magic = { 0x52, 0x6f, 0x66, 0x73 };

    byte[] data = new byte[4];
    disk.Read(data, 0, 4);

    return magic.SequenceEqual(data);
}

Many file systems have a magic that is used to identify a file system. Here we check for a specific array of bytes at a specific position on the disk, if the array matches the specific magic number then it is a Rofs. As you might have guessed this is not bullet proof, but it is very unlikely to fail.

/// <summary>
/// Checks if a file exists on the file system.
/// </summary>
/// <param name="path">The path of the file.</param>
/// <returns>true if the file exists; otherwise false.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="path"/> is null.
/// </exception>
/// <exception cref="ArgumentException">
/// The <paramref name="path"/> contains at least one non ISO-Latin-1 character.
/// </exception>
public override bool Exist(string path)
{
    if (path == null)
        throw new ArgumentNullException("path");
    else if (iso.GetByteCount(path) != path.Length)
        throw new ArgumentException("Contains a non ISO-Latin-1 character.", "path");

    foreach (FileEntry entry in GetFiles())
    {
        if (entry.Name == path)
            return true;
    }
    
    return false;
}

This method is really simple. Loop over all file entries and check if the names matches, no biggie.

/// <summary>
/// Gets all file entries in the file system.
/// </summary>
/// <returns>The next file entry in the system.</returns>
private IEnumerable<FileEntry> GetFiles()
{
    for (ushort i = 0; i < fileEntries; i++)
        yield return ReadStructure<FileEntry>(518 + 72 * i);
}

This method retrieves all file entries. A file entry is a struct that contains the name, position and size of a file. This is mainly used to retrieve files. The address 518 is the where the array of file entries start. 72 is the size of a file entry. Using these 2 numbers and knowing the total number of files on the disk we can easily find all file entries on the disk.

/// <summary>
/// Opens an already existing file.
/// </summary>
/// <param name="path">An absolute path for the file.</param>
/// <returns>The opened file stream.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="path"/> is null.
/// </exception>
/// <exception cref="ArgumentException">
/// The <paramref name="path"/> contains at least one non ISO-Latin-1 character.
/// </exception>
/// <exception cref="FileNotFoundException">
/// The file referenced by <paramref name="path"/> does
/// not exist on this system.
/// </exception>
public override Stream Open(string path)
{
    if (path == null)
        throw new ArgumentNullException("path");
    else if (iso.GetByteCount(path) != path.Length)
        throw new ArgumentException("Contains a non ISO-Latin-1 character.", "path");
    else if (!Exist(path))
        throw new FileNotFoundException("The file doesn't exist.", path);

    FileEntry entry = GetFile(path);
    if (entry != null)
    {
        FileStream file = new FileStream();
        
        byte[] data = ReadData(entry.Start, entry.Size);
        file.Write(data, 0, data.Length);
        file.Seek(0, SeekOrigin.Begin);

        return file;
    }
    else
        throw new FileNotFoundException("The file could not be found.", path);
}

First we need to find the file entry of the file we want to open. Then we check for its existence. Now that we know the file exists we open it for reading. We read the data from the file system and stores it in stream.

/// <summary>
/// Gets the file entry specified by the given <paramref name="path"/>.
/// </summary>
/// <returns>The file entry if it exists; otherwise null.</returns>
/// <param name="path">Path.</param>
private FileEntry GetFile(string path)
{
    foreach (FileEntry entry in GetFiles())
    {
        if (entry.Name == path)
            return entry;
    }
    
    return null;
}

This method simplifies the task of finding a specific file entry by looping over each one then matching their names against a given name.

/// <summary>
/// Creates a new rofs on the disk.
/// </summary>
/// <param name="destination">The stream where the file system is written to.</param>
/// <param name="files">
/// The files the file system should contain.
/// The key is the name of the file and the stream is the file contents.
/// </param>
/// <exception cref="ArgumentNullException">
/// <paramref name="destination"/> is null.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="files"/> contains to many.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="files"/> contains a file path
/// that contains at least one non ISO-Latin-1 character.
/// </exception>
public static void CreateDisk(Stream destination, IDictionary<string, Stream> files)
{
    if (destination == null)
        throw new ArgumentNullException("destination");
    else if (files == null)
        throw new ArgumentNullException("files");

    if (files.Count > ushort.MaxValue)
        throw new ArgumentOutOfRangeException("files", "Too many files.");

    byte[] bytes = { 0x52, 0x6f, 0x66, 0x73 };
    destination.Seek(512, SeekOrigin.Begin);
    destination.Write(bytes, 0, bytes.Length);

    bytes = BitConverter.GetBytes((ushort)files.Count);
    destination.Write(bytes, 0, bytes.Length);

    List<FileEntry> entries = new List<FileEntry>();
    uint start = 518 + (uint)(files.Count * 72);
    destination.Seek(start, SeekOrigin.Begin);

    foreach (KeyValuePair<string, Stream> file in files)
    {
        if (file.Key.Length != iso.GetByteCount(file.Key))
            throw new ArgumentException("Path \"" + file.Key + "\" contains a non ISO-Latin-1 character.", "files");

        FileEntry entry = new FileEntry();
        entry.Name = file.Key;
        entry.Start = start;
        entry.Size = (uint)file.Value.Length;   
        entries.Add(entry);

        start += entry.Size;
        file.Value.CopyTo(destination);
    }

    destination.Seek(518, SeekOrigin.Begin);
    for (int i = 0; i < entries.Count; i++)
    {
        bytes = Structures.ToBytes(entries[i]);
        destination.Write(bytes, 0, bytes.Length);
    }
}

Okay, this is the largest method. Because of the structure of Rofs it's much easier to create it when we know all the files that it should hold in advance. It is possible to add more files later on, but it requires massive re-arranging of the contents on the disk to do so.

However in this scenario we know about all file in advance. First we skip the first 512 bytes which are reserved for boot code. Next we write the magic number to the stream. Now we need to tell Rofs how many files it is going to contain. We know this by looking at the length of the files dictionary. 

Next we figure where the start of the file contents. Remember that we know that the file entries start at 518, they have the size of 72 bytes and file contents are placed right after the last file entry. The only varible in that equation is the number of file entries. From that we get the formula y = 72x + 518. Here x is the number of file entries and y is the address of the data area. Then we write the file contents by looping over all the given files. We make sure that the file names are valid. Now that we have the file entries we can write them to the disk which is a simple matter of converting each FileEntry to a byte[] and writing it to the disk. The order that the file entries are writting down in has no consequence of the validity of the disk. 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here