altos: Add SDCARD and FAT16 filesystem support
[fw/altos] / src / drivers / ao_fat.c
diff --git a/src/drivers/ao_fat.c b/src/drivers/ao_fat.c
new file mode 100644 (file)
index 0000000..a147616
--- /dev/null
@@ -0,0 +1,674 @@
+/*
+ * Copyright © 2013 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#ifndef AO_FAT_TEST
+#include "ao.h"
+#endif
+
+#include "ao_fat.h"
+#include "ao_bufio.h"
+
+/* Partition information, block numbers */
+
+static uint8_t partition_type;
+static uint32_t        partition_start, partition_end;
+
+/* File system parameters */
+static uint8_t sectors_per_cluster;
+static uint32_t        bytes_per_cluster;
+static uint16_t        reserved_sector_count;
+static uint8_t number_fat;
+static uint16_t        root_entries;
+static uint16_t sectors_per_fat;
+static uint32_t        fat_start;
+static uint32_t root_start;
+static uint32_t data_start;
+
+static uint32_t
+get_u32(uint8_t *base)
+{
+       return ((uint32_t) base[0] |
+               ((uint32_t) base[1] << 8) |
+               ((uint32_t) base[2] << 16) |
+               ((uint32_t) base[3] << 24));
+}
+
+static void
+put_u32(uint8_t *base, uint32_t value)
+{
+       base[0] = value;
+       base[1] = value >> 8;
+       base[2] = value >> 16;
+       base[3] = value >> 24;
+}
+
+static uint16_t
+get_u16(uint8_t *base)
+{
+       return ((uint16_t) base[0] | ((uint16_t) base[1] << 8));
+}
+
+static void
+put_u16(uint8_t *base, uint16_t value)
+{
+       base[0] = value;
+       base[1] = value >> 8;
+}
+
+static uint8_t
+ao_fat_cluster_valid(uint16_t cluster)
+{
+       return (2 <= cluster && cluster < 0xfff0);
+}
+
+/* Start using a block */
+static uint8_t *
+ao_fat_block_get(uint32_t block)
+{
+       block += partition_start;
+       if (block >= partition_end)
+               return NULL;
+       return ao_bufio_get(block);
+}
+
+/* Finish using a block, 'w' is 1 if modified */
+#define ao_fat_block_put(b,w) ao_bufio_put(b,w)
+
+/* Start using a root directory entry */
+static uint8_t *
+ao_fat_root_get(uint16_t e)
+{
+       uint32_t        byte = e * 0x20;
+       uint32_t        sector = byte >> 9;
+       uint16_t        offset = byte & 0x1ff;
+       uint8_t         *buf;
+
+       buf = ao_fat_block_get(root_start + sector);
+       if (!buf)
+               return NULL;
+       return buf + offset;
+}
+
+/* Finish using a root directory entry, 'w' is 1 if modified */
+static void
+ao_fat_root_put(uint8_t *root, uint16_t e, uint8_t write)
+{
+       uint16_t        offset = ((e * 0x20) & 0x1ff);
+       uint8_t         *buf = root - offset;
+
+       ao_fat_block_put(buf, write);
+}
+
+/* Get the next cluster entry in the chain */
+static uint16_t
+ao_fat_entry_read(uint16_t cluster)
+{
+       uint32_t        sector;
+       uint16_t        offset;
+       uint8_t         *buf;
+       uint16_t        ret;
+
+       if (!ao_fat_cluster_valid(cluster))
+               return 0xfff7;
+
+       cluster -= 2;
+       sector = cluster >> 8;
+       offset = (cluster << 1) & 0x1ff;
+       buf = ao_fat_block_get(fat_start + sector);
+       if (!buf)
+               return 0;
+       ret = buf[offset] | (buf[offset+1] << 8);
+       ao_fat_block_put(buf, 0);
+       return ret;
+}
+
+static uint16_t
+ao_fat_entry_replace(uint16_t  cluster, uint16_t new_value)
+{
+       uint32_t        sector;
+       uint16_t        offset;
+       uint8_t         *buf;
+       uint16_t        ret;
+       uint8_t         other_fats;
+
+       if (!ao_fat_cluster_valid(cluster))
+               return 0;
+
+       cluster -= 2;
+       sector = cluster >> 8;
+       offset = (cluster << 1) & 0x1ff;
+       buf = ao_fat_block_get(fat_start + sector);
+       if (!buf)
+               return 0;
+       ret = get_u16(buf + offset);
+       put_u16(buf + offset, new_value);
+       ao_fat_block_put(buf, 1);
+       for (other_fats = 1; other_fats < number_fat; other_fats++) {
+               buf = ao_fat_block_get(fat_start + other_fats * sectors_per_fat);
+               if (buf) {
+                       put_u16(buf + offset, new_value);
+                       ao_fat_block_put(buf, 1);
+               }
+       }
+       return ret;
+       
+}
+
+static void
+ao_fat_clear_cluster_chain(uint16_t cluster)
+{
+       while (ao_fat_cluster_valid(cluster))
+               cluster = ao_fat_entry_replace(cluster, 0x0000);
+}
+
+static uint16_t
+ao_fat_cluster_seek(uint16_t cluster, uint16_t distance)
+{
+       while (distance) {
+               cluster = ao_fat_entry_read(cluster);
+               if (!ao_fat_cluster_valid(cluster))
+                       break;
+               distance--;
+       }
+       return cluster;
+}
+
+static uint32_t
+ao_fat_sector_seek(uint16_t cluster, uint32_t sector)
+{
+       cluster = ao_fat_cluster_seek(cluster, sector / sectors_per_cluster);
+       if (!ao_fat_cluster_valid(cluster))
+               return 0xffffffff;
+       return data_start + (cluster-2) * sectors_per_cluster + sector % sectors_per_cluster;
+}
+
+/* Load the boot block and find the first partition */
+static uint8_t
+ao_fat_setup_partition(void)
+{
+       uint8_t *mbr;
+       uint8_t *partition;
+       uint32_t partition_size;
+
+       mbr = ao_bufio_get(0);
+       if (!mbr)
+               return 0;
+
+       /* Check the signature */
+       if (mbr[0x1fe] != 0x55 || mbr[0x1ff] != 0xaa) {
+               printf ("Invalid MBR signature %02x %02x\n",
+                       mbr[0x1fe], mbr[0x1ff]);
+               ao_bufio_put(mbr, 0);
+               return 0;
+       }
+
+       /* Just use the first partition */
+       partition = &mbr[0x1be];
+       
+       partition_type = partition[4];
+       switch (partition_type) {
+       case 4:         /* FAT16 up to 32M */
+       case 6:         /* FAT16 over 32M */
+               break;
+       case 0x0b:      /* FAT32 up to 2047GB */
+       case 0x0c:      /* FAT32 LBA */
+               break;
+       default:
+               printf ("Invalid partition type %02x\n", partition_type);
+               ao_bufio_put(mbr, 0);
+               return 0;
+       }
+
+       partition_start = get_u32(partition+8);
+       partition_size = get_u32(partition+12);
+       if (partition_size == 0) {
+               printf ("Zero-sized partition\n");
+               ao_bufio_put(mbr, 0);
+               return 0;
+       }
+       partition_end = partition_start + partition_size;
+       printf ("Partition type %02x start %08x end %08x\n",
+               partition_type, partition_start, partition_end);
+       ao_bufio_put(mbr, 0);
+       return 1;
+}
+       
+static uint8_t
+ao_fat_setup_fs(void)
+{
+       uint8_t *boot = ao_fat_block_get(0);
+
+       if (!boot)
+               return 0;
+
+       /* Check the signature */
+       if (boot[0x1fe] != 0x55 || boot[0x1ff] != 0xaa) {
+               printf ("Invalid BOOT signature %02x %02x\n",
+                       boot[0x1fe], boot[0x1ff]);
+               ao_bufio_put(boot, 0);
+               return 0;
+       }
+
+       /* Check the sector size */
+       if (get_u16(boot + 0xb) != 0x200) {
+               printf ("Invalid sector size %d\n",
+                       get_u16(boot + 0xb));
+               ao_bufio_put(boot, 0);
+               return 0;
+       }
+
+       sectors_per_cluster = boot[0xd];
+       bytes_per_cluster = sectors_per_cluster << 9;
+       reserved_sector_count = get_u16(boot+0xe);
+       number_fat = boot[0x10];
+       root_entries = get_u16(boot + 0x11);
+       sectors_per_fat = get_u16(boot+0x16);
+
+       printf ("sectors per cluster %d\n", sectors_per_cluster);
+       printf ("reserved sectors %d\n", reserved_sector_count);
+       printf ("number of FATs %d\n", number_fat);
+       printf ("root entries %d\n", root_entries);
+       printf ("sectors per fat %d\n", sectors_per_fat);
+
+       fat_start = reserved_sector_count;;
+       root_start = fat_start + number_fat * sectors_per_fat;
+       data_start = root_start + ((root_entries * 0x20 + 0x1ff) >> 9);
+
+       printf ("fat  start %d\n", fat_start);
+       printf ("root start %d\n", root_start);
+       printf ("data start %d\n", data_start);
+
+       return 1;
+}
+
+static uint8_t
+ao_fat_setup(void)
+{
+       if (!ao_fat_setup_partition())
+               return 0;
+       if (!ao_fat_setup_fs())
+               return 0;
+       return 1;
+}
+
+/*
+ * Low-level directory operations
+ */
+
+/*
+ * Basic file operations
+ */
+
+static struct ao_fat_dirent    ao_file_dirent;
+static uint32_t                ao_file_offset;
+
+static uint32_t
+ao_file_offset_to_sector(uint32_t offset)
+{
+       if (offset > ao_file_dirent.size)
+               return 0xffffffff;
+       return ao_fat_sector_seek(ao_file_dirent.cluster, offset >> 9);
+}
+
+uint8_t
+ao_fat_open(char name[11])
+{
+       uint16_t                entry = 0;
+       struct ao_fat_dirent    dirent;
+
+       while (ao_fat_readdir(&entry, &dirent)) {
+               if (!memcmp(name, dirent.name, 11)) {
+                       ao_file_dirent = dirent;
+                       ao_file_offset = 0;
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+
+
+static uint8_t
+ao_fat_set_size(uint32_t size)
+{
+       uint16_t        clear_cluster = 0;
+       uint8_t         *dent;
+       uint16_t        first_cluster;
+
+       first_cluster = ao_file_dirent.cluster;
+       printf ("set size to %d\n", size);
+       if (size == ao_file_dirent.size)
+               return 1;
+       if (size == 0) {
+               printf ("erase file\n");
+               clear_cluster = ao_file_dirent.cluster;
+               first_cluster = 0;
+       } else {
+               uint16_t        new_num;
+               uint16_t        old_num;
+
+               new_num = (size + bytes_per_cluster - 1) / bytes_per_cluster;
+               old_num = (ao_file_dirent.size + bytes_per_cluster - 1) / bytes_per_cluster;
+               if (new_num < old_num) {
+                       uint16_t last_cluster;
+
+                       printf("Remove %d clusters\n", old_num - new_num);
+                       /* Go find the last cluster we want to preserve in the file */
+                       last_cluster = ao_fat_cluster_seek(ao_file_dirent.cluster, new_num - 1);
+
+                       printf ("Last cluster is now %04x\n", last_cluster);
+                       /* Rewrite that cluster entry with 0xffff to mark the end of the chain */
+                       clear_cluster = ao_fat_entry_replace(last_cluster, 0xffff);
+               } else if (new_num > old_num) {
+                       uint16_t        need;
+                       uint16_t        free;
+                       uint16_t        last_cluster;
+
+                       if (old_num)
+                               last_cluster = ao_fat_cluster_seek(ao_file_dirent.cluster, old_num - 1);
+                       else
+                               last_cluster = 0;
+
+                       need = new_num - old_num;
+                       printf ("Need %d clusters\n", need);
+                       /* See if there are enough free clusters in the file system */
+                       for (free = 2; need > 0 && (free - 2) < sectors_per_fat * 256; free++) {
+                               if (!ao_fat_entry_read(free)) {
+                                       printf ("\tCluster %04x available\n", free);
+                                       need--;
+                               }
+                       }
+                       /* Still need some, tell the user that we've failed */
+                       if (need) {
+                               printf ("File system full\n");
+                               return 0;
+                       }
+
+                       need = new_num - old_num;
+                       /* Now go allocate those clusters */
+                       for (free = 2; need > 0 && (free - 2) < sectors_per_fat * 256; free++) {
+                               if (!ao_fat_entry_read(free)) {
+                                       printf ("\tAllocate %04x\n", free);
+                                       if (last_cluster)
+                                               ao_fat_entry_replace(last_cluster, free);
+                                       else
+                                               first_cluster = free;
+                                       last_cluster = free;
+                                       need--;
+                               }
+                       }
+                       /* Mark the new end of the chain */
+                       ao_fat_entry_replace(last_cluster, 0xffff);
+               }
+       }
+
+       /* Deallocate clusters off the end of the file */
+       if (ao_fat_cluster_valid(clear_cluster)) {
+               printf ("Clear clusters starting with %04x\n", clear_cluster);
+               ao_fat_clear_cluster_chain(clear_cluster);
+       }
+
+       dent = ao_fat_root_get(ao_file_dirent.entry);
+       if (!dent)
+               return 0;
+       put_u32(dent + 0x1c, size);
+       put_u16(dent + 0x1a, first_cluster);
+       ao_fat_root_put(dent, ao_file_dirent.entry, 1);
+       ao_file_dirent.size = size;
+       ao_file_dirent.cluster = first_cluster;
+       return 1;
+}
+
+uint8_t
+ao_fat_creat(char name[11])
+{
+       uint16_t        entry;
+
+       if (ao_fat_open(name))
+               return ao_fat_set_size(0);
+
+       for (entry = 0; entry < root_entries; entry++) {
+               uint8_t *dent = ao_fat_root_get(entry);
+
+               if (dent[0] == AO_FAT_DENT_EMPTY ||
+                   dent[0] == AO_FAT_DENT_END) {
+                       memmove(dent, name, 11);
+                       dent[0x0b] = 0x00;
+                       dent[0x0c] = 0x00;
+                       dent[0x0d] = 0x00;
+                       /* XXX fix time */
+                       put_u16(dent + 0x0e, 0);
+                       /* XXX fix date */
+                       put_u16(dent + 0x10, 0);
+                       /* XXX fix date */
+                       put_u16(dent + 0x12, 0);
+                       /* XXX FAT32 high cluster bytes */
+                       put_u16(dent + 0x14, 0);
+                       /* XXX fix time */
+                       put_u16(dent + 0x16, 0);
+                       /* XXX fix date */
+                       put_u16(dent + 0x18, 0);
+                       /* cluster number */
+                       put_u16(dent + 0x1a, 0);
+                       /* size */
+                       put_u32(dent + 0x1c, 0);
+                       ao_fat_root_put(dent, entry, 1);
+                       return ao_fat_open(name);
+               }
+       }
+       return 0;
+}
+
+void
+ao_fat_close(void)
+{
+       memset(&ao_file_dirent, '\0', sizeof (struct ao_fat_dirent));
+       ao_bufio_flush();
+}
+
+int
+ao_fat_read(uint8_t *dest, int len)
+{
+       uint32_t        sector;
+       uint16_t        this_time;
+       uint16_t        offset;
+       uint8_t         *buf;
+       int             ret = 0;
+
+       if (ao_file_offset + len > ao_file_dirent.size)
+               len = ao_file_dirent.size - ao_file_offset;
+
+       while (len) {
+               offset = ao_file_offset & 0x1ff;
+               if (offset + len < 512)
+                       this_time = len;
+               else
+                       this_time = 512 - offset;
+
+               sector = ao_file_offset_to_sector(ao_file_offset);
+               if (sector == 0xffffffff)
+                       break;
+               buf = ao_fat_block_get(sector);
+               if (!buf)
+                       break;
+               memcpy(dest, buf + offset, this_time);
+               ao_fat_block_put(buf, 0);
+
+               ret += this_time;
+               len -= this_time;
+               dest += this_time;
+               ao_file_offset += this_time;
+       }
+       return ret;
+}
+
+int
+ao_fat_write(uint8_t *src, int len)
+{
+       uint32_t        sector;
+       uint16_t        this_time;
+       uint16_t        offset;
+       uint8_t         *buf;
+       int             ret = 0;
+
+       if (ao_file_offset + len > ao_file_dirent.size) {
+               if (!ao_fat_set_size(ao_file_offset + len))
+                       return 0;
+       }
+
+       while (len) {
+               offset = ao_file_offset & 0x1ff;
+               if (offset + len < 512)
+                       this_time = len;
+               else
+                       this_time = 512 - offset;
+
+               sector = ao_file_offset_to_sector(ao_file_offset);
+               if (sector == 0xffffffff)
+                       break;
+               buf = ao_fat_block_get(sector);
+               if (!buf)
+                       break;
+               memcpy(buf + offset, src, this_time);
+               ao_fat_block_put(buf, 1);
+
+               ret += this_time;
+               len -= this_time;
+               src += this_time;
+               ao_file_offset += this_time;
+       }
+       return 0;
+}
+
+uint32_t
+ao_fat_seek(int32_t pos, uint8_t whence)
+{
+       switch (whence) {
+       case AO_FAT_SEEK_SET:
+               ao_file_offset = pos;
+               break;
+       case AO_FAT_SEEK_CUR:
+               ao_file_offset += pos;
+               break;
+       case AO_FAT_SEEK_END:
+               ao_file_offset = ao_file_dirent.size + pos;
+               break;
+       }
+       if (ao_file_offset > ao_file_dirent.size)
+               ao_fat_set_size(ao_file_offset);
+       return ao_file_offset;
+}
+
+uint8_t
+ao_fat_unlink(char name[11])
+{
+       uint16_t                entry = 0;
+       struct ao_fat_dirent    dirent;
+
+       while (ao_fat_readdir(&entry, &dirent)) {
+               if (memcmp(name, dirent.name, 11) == 0) {
+                       uint8_t *next;
+                       uint8_t *ent;
+                       uint8_t delete;
+                       ao_fat_clear_cluster_chain(dirent.cluster);
+                       next = ao_fat_root_get(dirent.entry + 1);
+                       if (next && next[0] != AO_FAT_DENT_END)
+                               delete = AO_FAT_DENT_EMPTY;
+                       else
+                               delete = AO_FAT_DENT_END;
+                       if (next)
+                               ao_fat_root_put(next, dirent.entry + 1, 0);
+                       ent = ao_fat_root_get(dirent.entry);
+                       if (ent) {
+                               memset(ent, '\0', 0x20);
+                               *ent = delete;
+                               ao_fat_root_put(ent, dirent.entry, 1);
+                       }
+                       ao_bufio_flush();
+                       return 1;
+               }
+       }
+       return 0;
+}
+
+uint8_t
+ao_fat_rename(char old[11], char new[11])
+{
+       return 0;
+}
+
+uint8_t
+ao_fat_readdir(uint16_t *entry, struct ao_fat_dirent *dirent)
+{
+       uint8_t *dent;
+
+       if (*entry >= root_entries)
+               return 0;
+       for (;;) {
+               dent = ao_fat_root_get(*entry);
+
+               if (dent[0] == AO_FAT_DENT_END) {
+                       ao_fat_root_put(dent, *entry, 0);
+                       return 0;
+               }
+               if (dent[0] != AO_FAT_DENT_EMPTY &&
+                   (dent[0x0b] & (AO_FAT_FILE_DIRECTORY|AO_FAT_FILE_VOLUME_LABEL)) == 0)
+                       break;
+               ao_fat_root_put(dent, *entry, 0);
+               (*entry)++;
+       }
+       memcpy(dirent->name, dent, 11);
+       dirent->attr = dent[0xb];
+       dirent->size = get_u32(dent+0x1c);
+       dirent->cluster = get_u16(dent+0x1a);
+       dirent->entry = *entry;
+       ao_fat_root_put(dent, *entry, 0);
+       (*entry)++;
+       return 1;
+}
+
+static void
+ao_fat_list(void)
+{
+       uint16_t                entry = 0;
+       struct ao_fat_dirent    dirent;
+
+       while (ao_fat_readdir(&entry, &dirent)) {
+               printf ("%-8.8s.%-3.3s %02x %d\n",
+                       dirent.name, dirent.name + 8, dirent.attr, dirent.size);
+       }
+}
+
+static void
+ao_fat_test(void)
+{
+       ao_fat_setup();
+       ao_fat_list();
+}
+
+static const struct ao_cmds ao_fat_cmds[] = {
+       { ao_fat_test,  "F\0Test FAT" },
+       { 0, NULL },
+};
+
+void
+ao_fat_init(void)
+{
+       ao_bufio_init();
+       ao_cmd_register(&ao_fat_cmds[0]);
+}
+