/*
   Linux loop device with checksums

   Copyright (C)  2008 Timo Juhani Lindfors <timo.lindfors@iki.fi>

   This module 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; either version 2 of the License, or
   (at your option) any later version.

   This module 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 module; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/*
   Overall idea as pseudocode:

   # state of the system (stored in RAM):
   
   checksum = []
   checksum_present = []

   # operations:
   
   write_block(b):
     checksum[b] = calculate_checksum(b)     
     write_to_disk(b)

   read_block(b):
     read_from_disk(b)
     s = calculate_checksum(b)
     if checksum_present[b] and s != checksum[b]:
       print warning message
     
     if !checksum_present[b]:
       checksum[b] = s
       checksum_present[b] = True
   
*/

/*
   sudo aptitude install linux-kbuild-2.6.18 linux-headers-`uname -r`
   make
   sudo modprobe loop
   sudo insmod crcloop.ko checksum_bits=8 debug=1
   sudo losetup -p 0 -e 7 /dev/loop0 test1.img < /dev/null
   sudo cat /dev/loop0 > /dev/null # to generate checksums
   # ... operate on /dev/loop0 ...
   sudo losetup -d /dev/loop0
   sudo rmmod crcloop
*/

#include <linux/crc32.h>
#include <linux/loop.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/vmalloc.h>

#define LOOP_CRYPT_CRC 7
#define MAX_CORRUPTIONS 8

#define FLOW(fmt, args...) do{if(debug>0){printk(KERN_DEBUG "%s:%d: " fmt "\n", __FUNCTION__, __LINE__, ##args);}}while(0)

typedef struct {
    void *checksum;
    loff_t num_checksums;
    __u8 *corruption;
    int corruption_head;
    int lo_number;
} crcloop_state_t;

static int debug = 0;
static int checksum_bits = 32;

static int crcloop_init(struct loop_device *lo, const struct loop_info64 *info) {
    crcloop_state_t *state;
    loff_t len;

    FLOW();
    state = kmalloc(sizeof(crcloop_state_t), GFP_KERNEL);
    if (state == NULL) {
        return -ENOMEM;
    }

    len = i_size_read(lo->lo_backing_file->f_mapping->host);
    state->num_checksums = (len + 4096 - 1) / 4096;
    FLOW("len %llu num_checksums %llu", len, state->num_checksums);
    
    state->checksum = vmalloc(state->num_checksums*(checksum_bits / 8));
    if (state->checksum == NULL) {
        kfree(state);
        return -ENOMEM;
    }

    state->corruption = vmalloc(MAX_CORRUPTIONS*4096);
    if (state->corruption == NULL) {
        vfree(state->checksum);
        kfree(state);
        return -ENOMEM;
    }

    state->corruption_head = 0;
    state->lo_number = lo->lo_number;

    memset(state->checksum, 0, state->num_checksums*(checksum_bits / 8));
    FLOW("state->checksum %p", state->checksum);
    
    lo->key_data = state;
    return 0;
}

static inline __u32 checksum_get(crcloop_state_t *state, loff_t block) {
    if (checksum_bits == 32) {
        return ((__u32*)(state->checksum))[block];
    } else if (checksum_bits == 16) {
        return ((__u16*)(state->checksum))[block];
    } else if (checksum_bits == 8) {
        return ((__u8*)(state->checksum))[block];
    } else {
        printk(KERN_ERR "BUG: crcloop: invalid checksum_bits: %d\n",
               checksum_bits);
        return 0;
    }
}

static inline void checksum_put(crcloop_state_t *state, loff_t block, __u32 sum) {
    if (checksum_bits == 32) {
        ((__u32*)state->checksum)[block] = sum;
    } else if (checksum_bits == 16) {
        ((__u16*)state->checksum)[block] = sum;
    } else if (checksum_bits == 8) {
        ((__u8*)state->checksum)[block] = sum;
    } else {
        printk(KERN_ERR "BUG: crcloop: invalid checksum_bits: %d\n",
               checksum_bits);
    }
}

static inline __u32 checksum_calculate(crcloop_state_t *state, const char *buf) {
    __u32 sum;

    sum = crc32(~0, buf, 4096);
    if (sum == 0) { /* 0 is reserved to mean that sum has not been calculated */
        sum++;
    }
    if (checksum_bits == 32) {
        return sum;
    } else if (checksum_bits == 16) {
        return sum & 0xffff;
    } else if (checksum_bits == 8) {
        return sum & 0xff;
    } else {
        printk(KERN_ERR "BUG: crcloop: invalid checksum_bits: %d\n",
               checksum_bits);
        return 0;
    }
}

static void crcloop_check(int cmd, loff_t block, crcloop_state_t *state,
                          const char *buf) {
    __u32 sum;

    sum = checksum_calculate(state, buf);

    if (block < 0 || block >= state->num_checksums) {
        printk(KERN_ERR "BUG: crcloop: block %llu out of bounds (num_checksums %llu)\n",
               block, state->num_checksums);
        return;
    }

    if (debug > 1) {
        FLOW("%s %llu %08x %08x",
             cmd == READ ? "read" : "write",
             block,
             sum,
             checksum_get(state, block));
    }

    if (cmd == READ) {
        if (checksum_get(state, block) && checksum_get(state, block) != sum) {
            printk(KERN_ERR "crcloop%d: block %llu has checksum %08x but it should have %08x, see %p\n",
                   state->lo_number,
                   block,
                   sum,
                   checksum_get(state, block),
                   state->corruption + state->corruption_head*4096);
            memcpy(state->corruption + state->corruption_head*4096,
                   buf,
                   4096);
            state->corruption_head = (state->corruption_head + 1) % MAX_CORRUPTIONS;
        }
    }
    checksum_put(state, block, sum);
}

static int crcloop_transfer(struct loop_device *lo, int cmd,
                            struct page *raw_page, unsigned raw_off,
                            struct page *loop_page, unsigned loop_off,
                            int size, sector_t real_block) {
    char *raw_buf = kmap_atomic(raw_page, KM_USER0) + raw_off;
    char *loop_buf = kmap_atomic(loop_page, KM_USER1) + loop_off;

    if (cmd == READ) {
        memcpy(loop_buf, raw_buf, size);
    } else {
        memcpy(raw_buf, loop_buf, size);
    }

    if (size == 4096) {
        crcloop_check(cmd,
                      real_block / 8, /* 512-byte blocks to 4096-byte blocks */
                      (crcloop_state_t*)lo->key_data,
                      raw_buf);
    } else {
        printk(KERN_WARNING "crcloop: ignoring checks: cmd %s size %d real_block %llu\n",
               cmd == READ ? "read" : "write",
               size, real_block);
    }
    
    kunmap_atomic(raw_buf, KM_USER0);
    kunmap_atomic(loop_buf, KM_USER1);
    cond_resched();
    return 0;
}

static int crcloop_release(struct loop_device *lo) {
    crcloop_state_t *state;

    FLOW();
    state = lo->key_data;
    vfree(state->corruption);
    FLOW();
    vfree(state->checksum);
    FLOW();
    kfree(state);
    FLOW();
    return 0;
}

static struct loop_func_table crcloop_funcs = {
    .number = LOOP_CRYPT_CRC,
    .init = crcloop_init,
    .transfer = crcloop_transfer,
    .release = crcloop_release,
    .owner = THIS_MODULE
};

static int __init init_crcloop(void) {
    int ret;

    FLOW("checksum_bits %d", checksum_bits);
    if (checksum_bits != 32 && checksum_bits != 16 && checksum_bits != 8) {
        return -EINVAL;
    }
    ret = loop_register_transfer(&crcloop_funcs);
    FLOW("ret = %d", ret);
    return ret;
}

static void __exit cleanup_crcloop(void) {
    int ret;
    
    FLOW();
    ret = loop_unregister_transfer(LOOP_CRYPT_CRC);
    FLOW("ret = %d", ret);
}

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("loop device with crc checksums");
MODULE_AUTHOR("Timo Juhani Lindfors <timo.lindfors@iki.fi>");
module_param(debug, int, 0);
MODULE_PARM_DESC(debug, "Verbosity of debug messages");
module_param(checksum_bits, int, 0);
MODULE_PARM_DESC(checksum_bits, "Size of block checksum in bits (must be 16 or 32)");
module_init(init_crcloop);
module_exit(cleanup_crcloop);
