#!/usr/bin/env python import logging import os from time import time from errno import ENOENT from fuse import FUSE, FuseOSError, Operations, LoggingMixIn from stat import S_IFDIR, S_IFREG BLOCK_SIZE = 512 SUPERBLOCK_ADDR = 4096 * BLOCK_SIZE BITMAP_ADDR = SUPERBLOCK_ADDR + 2 * BLOCK_SIZE ENTRY_SIZE = (1 + 4 + 4 + 8 + 1) class MorosFuse(LoggingMixIn, Operations): chmod = None chown = None readlink = None rename = None rmdir = None symlink = None truncate = None utimens = None getxattr = None def __init__(self, path): self.block_size = BLOCK_SIZE self.image = open(path, "r+b") self.image.seek(SUPERBLOCK_ADDR) superblock = self.image.read(self.block_size) assert superblock[0:8] == b"MOROS FS" # Signature assert superblock[8] == 1 # Version assert self.block_size == 2 << (8 + superblock[9]) self.block_count = int.from_bytes(superblock[10:14], "big") self.alloc_count = int.from_bytes(superblock[14:18], "big") bs = 8 * self.block_size # number of bits per bitmap block total = self.block_count rest = (total - (BITMAP_ADDR // self.block_size)) * bs // (bs + 1) self.data_addr = BITMAP_ADDR + (rest // bs) * self.block_size def __next_free_addr(self): for bitmap_addr in range(BITMAP_ADDR, self.data_addr): self.image.seek(bitmap_addr) bitmap = self.image.read(self.block_size) for i in range(self.block_size): byte = bitmap[i] for bit in range(0, 8): if (byte >> bit & 1) == 0: block = (bitmap_addr - BITMAP_ADDR) * self.block_size * 8 + i * 8 + bit return self.data_addr + block * self.block_size def __is_alloc(self, addr): block = (addr - self.data_addr) // self.block_size bitmap_addr = BITMAP_ADDR + (block // (self.block_size * 8)) pos = bitmap_addr + (block // 8) self.image.seek(pos) byte = int.from_bytes(self.image.read(1), "big") bit = block % 8 return (byte >> bit & 1) == 1 def __alloc(self, addr): block = (addr - self.data_addr) // self.block_size bitmap_addr = BITMAP_ADDR + (block // (self.block_size * 8)) self.image.seek(bitmap_addr + (block // 8)) byte = int.from_bytes(self.image.read(1), "big") self.image.seek(-1, 1) bit = block % 8 byte |= (1 << bit) self.image.write(bytes([byte])) def __free(self, addr): block = (addr - self.data_addr) // self.block_size bitmap_addr = BITMAP_ADDR + (block // (self.block_size * 8)) self.image.seek(bitmap_addr + (block // 8)) byte = int.from_bytes(self.image.read(1), "big") self.image.seek(-1, 1) bit = block % 8 byte &= ~(1 << bit) self.image.write(bytes([byte])) def __update_dir_size(self, path, entries): entries.remove(".") entries.remove("..") (_, parent_addr, parent_size, _, parent_name) = self.__scan(path) parent_size = ENTRY_SIZE * len(entries) + len("".join(entries)) self.image.seek(-(4 + 8 + 1 + len(parent_name)), 1) self.image.write(parent_size.to_bytes(4, "big")) return parent_addr def __scan(self, path): next_block_addr = self.data_addr res = (0, next_block_addr, 0, 0, "") # Root dir for d in path[1:].split("/"): if d == "": return res res = (0, 0, 0, 0, "") # Not found for (kind, addr, size, time, name) in self.__read(next_block_addr): if name == d: res = (kind, addr, size, time, name) next_block_addr = addr break return res def __read(self, next_block_addr): while next_block_addr != 0: self.image.seek(next_block_addr) next_block_addr = int.from_bytes(self.image.read(4), "big") * self.block_size offset = 4 while offset < self.block_size: kind = int.from_bytes(self.image.read(1), "big") addr = int.from_bytes(self.image.read(4), "big") * self.block_size size = int.from_bytes(self.image.read(4), "big") time = int.from_bytes(self.image.read(8), "big") n = int.from_bytes(self.image.read(1), "big") if n == 0: self.image.seek(-(1 + 4 + 4 + 8 + 1), 1) # Rewind to end of previous entry break name = self.image.read(n).decode("utf-8") offset += 1 + 4 + 4 + 8 + 1 + n if addr > 0: yield (kind, addr, size, time, name) def destroy(self, path): self.image.close() return def getattr(self, path, fh=None): (kind, addr, size, time, name) = self.__scan(path) if addr == 0: raise FuseOSError(ENOENT) mode = S_IFDIR | 0o755 if kind == 0 else S_IFREG | 0o644 return { "st_atime": 0, "st_mtime": time, "st_uid": 0, "st_gid": 0, "st_mode": mode, "st_size": size } def read(self, path, size, offset, fh): (kind, next_block_addr, size, time, name) = self.__scan(path) res = b"" while next_block_addr != 0 and size > 0: self.image.seek(next_block_addr) next_block_addr = int.from_bytes(self.image.read(4), "big") * self.block_size if offset < self.block_size - 4: buf = self.image.read(max(0, min(self.block_size - 4, size))) res = b"".join([res, buf[offset:]]) offset = 0 else: offset -= self.block_size - 4 size -= self.block_size - 4 return res def readdir(self, path, fh): files = [".", ".."] (_, next_block_addr, _, _, _) = self.__scan(path) for (kind, addr, size, time, name) in self.__read(next_block_addr): files.append(name) return files def mkdir(self, path, mode): self.create(path, S_IFDIR | mode) def create(self, path, mode): (path, _, name) = path.rpartition("/") entries = self.readdir(path + "/", 0) entries.append(name) pos = self.image.tell() parent_addr = self.__update_dir_size(path, entries) # Allocate space for the new dir entry if needed blocks = 0 addr = parent_addr next_addr = parent_addr while next_addr != 0: addr = next_addr blocks += 1 self.image.seek(addr) next_addr = int.from_bytes(self.image.read(4), "big") * self.block_size free_size = (self.block_size - 4) - (pos - addr) if free_size < ENTRY_SIZE + len(name): pos = self.image.tell() - 4 addr = self.__next_free_addr() self.__alloc(addr) self.image.seek(pos) self.image.write((addr // self.block_size).to_bytes(4, "big")) pos = addr + 4 # Allocate space for the new file kind = int((mode & S_IFDIR) != S_IFDIR) size = 0 addr = self.__next_free_addr() self.__alloc(addr) # Add dir entry self.image.seek(pos) self.image.write(kind.to_bytes(1, "big")) self.image.write((addr // self.block_size).to_bytes(4, "big")) self.image.write(size.to_bytes(4, "big")) self.image.write(int(time()).to_bytes(8, "big")) self.image.write(len(name).to_bytes(1, "big")) self.image.write(name.encode("utf-8")) return 0 def write(self, path, data, offset, fh): (_, addr, size, _, name) = self.__scan(path) n = self.block_size - 4 # Space available for data in blocks j = size % n # Start of space available in last block # Update file size self.image.seek(-(4 + 8 + 1 + len(name)), 1) size = max(size, offset + len(data)) self.image.write(size.to_bytes(4, "big")) for i in range(0, offset, n): self.image.seek(addr) next_addr = int.from_bytes(self.image.read(4), "big") * self.block_size if i + n >= offset: self.image.seek(addr + 4 + j) self.image.write(data[0:(n - j)]) if next_addr == 0: next_addr = self.__next_free_addr() self.__alloc(next_addr) self.image.seek(addr) self.image.write((next_addr // self.block_size).to_bytes(4, "big")) addr = next_addr for i in range(n - j if j > 0 else 0, len(data), n): next_addr = 0 if i + n < len(data): # TODO: check for off by one error next_addr = self.__next_free_addr() self.__alloc(next_addr) self.image.seek(addr) self.image.write((next_addr // self.block_size).to_bytes(4, "big")) self.image.write(data[i:min(i + n, len(data))]) addr = next_addr return len(data) def unlink(self, path): # Unlink and free blocs (_, addr, size, _, name) = self.__scan(path) self.image.seek(-(4 + 4 + 8 + 1 + len(name)), 1) self.image.write((0).to_bytes(4, "big")) while addr != 0: self.__free(addr) self.image.seek(addr) addr = int.from_bytes(self.image.read(4), "big") * self.block_size # Update parent dir size (path, _, _) = path.rpartition("/") entries = self.readdir(path + "/", 0) self.__update_dir_size(path, entries) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('image') parser.add_argument('mount') args = parser.parse_args() #logging.basicConfig(level=logging.DEBUG) fuse = FUSE(MorosFuse(args.image), args.mount, ro=False, foreground=True, allow_other=True)