/*****************************************************************************

            (c) Cambridge Silicon Radio Limited 2012
            All rights reserved and confidential information of CSR

            Refer to LICENSE.txt included with this source for details
            on the license terms.

*****************************************************************************/

/*
 * ---------------------------------------------------------------------------
 * FILE: csr_wifi_hip_xbv.c
 *
 * PURPOSE:
 *      Routines for downloading firmware to UniFi.
 *
 *      UniFi firmware files use a nested TLV (Tag-Length-Value) format.
 *
 * ---------------------------------------------------------------------------
 */
#include <linux/slab.h>

#ifdef CSR_WIFI_XBV_TEST
/* Standalone test harness */
#include "unifi_xbv.h"
#include "csr_wifi_hip_unifihw.h"
#else
/* Normal driver build */
#include "csr_wifi_hip_unifiversion.h"
#include "csr_wifi_hip_card.h"
#define DBG_TAG(t)
#endif

#include "csr_wifi_hip_xbv.h"

#define STREAM_CHECKSUM 0x6d34        /* Sum of uint16s in each patch stream */

/* XBV sizes used in patch conversion
 */
#define PTDL_MAX_SIZE 2048            /* Max bytes allowed per PTDL */
#define PTDL_HDR_SIZE (4 + 2 + 6 + 2) /* sizeof(fw_id, sec_len, patch_cmd, csum) */

/* Struct to represent a buffer for reading firmware file */

typedef struct
{
    void      *dlpriv;
    s32   ioffset;
    fwreadfn_t iread;
} ct_t;

/* Struct to represent a TLV field */
typedef struct
{
    char t_name[4];
    u32     t_len;
} tag_t;


#define TAG_EQ(i, v)    (((i)[0] == (v)[0]) &&  \
                         ((i)[1] == (v)[1]) &&  \
                         ((i)[2] == (v)[2]) &&  \
                         ((i)[3] == (v)[3]))

/* We create a small stack on the stack that contains an enum
 * indicating the containing list segments, and the offset at which
 * those lists end.  This enables a lot more error checking. */
typedef enum
{
    xbv_xbv1,
    /*xbv_info,*/
    xbv_fw,
    xbv_vers,
    xbv_vand,
    xbv_ptch,
    xbv_other
} xbv_container;

#define XBV_STACK_SIZE 6
#define XBV_MAX_OFFS   0x7fffffff

typedef struct
{
    struct
    {
        xbv_container container;
        s32      ioffset_end;
    } s[XBV_STACK_SIZE];
    u32 ptr;
} xbv_stack_t;

static s32 read_tag(card_t *card, ct_t *ct, tag_t *tag);
static s32 read_bytes(card_t *card, ct_t *ct, void *buf, u32 len);
static s32 read_uint(card_t *card, ct_t *ct, u32 *u, u32 len);
static s32 xbv_check(xbv1_t *fwinfo, const xbv_stack_t *stack,
                          xbv_mode new_mode, xbv_container old_cont);
static s32 xbv_push(xbv1_t *fwinfo, xbv_stack_t *stack,
                         xbv_mode new_mode, xbv_container old_cont,
                         xbv_container new_cont, u32 ioff);

static u32 write_uint16(void *buf, const u32 offset,
                              const u16 val);
static u32 write_uint32(void *buf, const u32 offset,
                              const u32 val);
static u32 write_bytes(void *buf, const u32 offset,
                             const u8 *data, const u32 len);
static u32 write_tag(void *buf, const u32 offset,
                           const char *tag_str);
static u32 write_chunk(void *buf, const u32 offset,
                             const char *tag_str,
                             const u32 payload_len);
static u16 calc_checksum(void *buf, const u32 offset,
                               const u32 bytes_len);
static u32 calc_patch_size(const xbv1_t *fwinfo);

static u32 write_xbv_header(void *buf, const u32 offset,
                                  const u32 file_payload_length);
static u32 write_ptch_header(void *buf, const u32 offset,
                                   const u32 fw_id);
static u32 write_patchcmd(void *buf, const u32 offset,
                                const u32 dst_genaddr, const u16 len);
static u32 write_reset_ptdl(void *buf, const u32 offset,
                                  const xbv1_t *fwinfo, u32 fw_id);
static u32 write_fwdl_to_ptdl(void *buf, const u32 offset,
                                    fwreadfn_t readfn, const struct FWDL *fwdl,
                                    const void *fw_buf, const u32 fw_id,
                                    void *rdbuf);

/*
 * ---------------------------------------------------------------------------
 *  parse_xbv1
 *
 *      Scan the firmware file to find the TLVs we are interested in.
 *      Actions performed:
 *        - check we support the file format version in VERF
 *      Store these TLVs if we have a firmware image:
 *        - SLTP Symbol Lookup Table Pointer
 *        - FWDL firmware download segments
 *        - FWOL firmware overlay segment
 *        - VMEQ Register probe tests to verify matching h/w
 *      Store these TLVs if we have a patch file:
 *        - FWID the firmware build ID that this file patches
 *        - PTDL The actual patches
 *
 *      The structure pointed to by fwinfo is cleared and
 *      'fwinfo->mode' is set to 'unknown'.  The 'fwinfo->mode'
 *      variable is set to 'firmware' or 'patch' once we know which
 *      sort of XBV file we have.
 *
 *  Arguments:
 *      readfn          Pointer to function to call to read from the file.
 *      dlpriv          Opaque pointer arg to pass to readfn.
 *      fwinfo          Pointer to fwinfo struct to fill in.
 *
 *  Returns:
 *      CSR_RESULT_SUCCESS on success, CSR error code on failure
 * ---------------------------------------------------------------------------
 */
CsrResult xbv1_parse(card_t *card, fwreadfn_t readfn, void *dlpriv, xbv1_t *fwinfo)
{
    ct_t ct;
    tag_t tag;
    xbv_stack_t stack;

    ct.dlpriv = dlpriv;
    ct.ioffset = 0;
    ct.iread = readfn;

    memset(fwinfo, 0, sizeof(xbv1_t));
    fwinfo->mode = xbv_unknown;

    /* File must start with XBV1 triplet */
    if (read_tag(card, &ct, &tag) <= 0)
    {
        unifi_error(NULL, "File is not UniFi firmware\n");
        return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
    }

    DBG_TAG(tag.t_name);

    if (!TAG_EQ(tag.t_name, "XBV1"))
    {
        unifi_error(NULL, "File is not UniFi firmware (%s)\n", tag.t_name);
        return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
    }

    stack.ptr = 0;
    stack.s[stack.ptr].container = xbv_xbv1;
    stack.s[stack.ptr].ioffset_end = XBV_MAX_OFFS;

    /* Now scan the file */
    while (1)
    {
        s32 n;

        n = read_tag(card, &ct, &tag);
        if (n < 0)
        {
            unifi_error(NULL, "No tag\n");
            return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
        }
        if (n == 0)
        {
            /* End of file */
            break;
        }

        DBG_TAG(tag.t_name);

        /* File format version */
        if (TAG_EQ(tag.t_name, "VERF"))
        {
            u32 version;

            if (xbv_check(fwinfo, &stack, xbv_unknown, xbv_xbv1) ||
                (tag.t_len != 2) ||
                read_uint(card, &ct, &version, 2))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }
            if (version != 0)
            {
                unifi_error(NULL, "Unsupported firmware file version: %d.%d\n",
                            version >> 8, version & 0xFF);
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }
        }
        else if (TAG_EQ(tag.t_name, "LIST"))
        {
            char name[4];
            u32 list_end;

            list_end = ct.ioffset + tag.t_len;

            if (read_bytes(card, &ct, name, 4))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            DBG_TAG(name);
            if (TAG_EQ(name, "FW  "))
            {
                if (xbv_push(fwinfo, &stack, xbv_firmware, xbv_xbv1, xbv_fw, list_end))
                {
                    return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
                }
            }
            else if (TAG_EQ(name, "VERS"))
            {
                if (xbv_push(fwinfo, &stack, xbv_firmware, xbv_fw, xbv_vers, list_end) ||
                    (fwinfo->vers.num_vand != 0))
                {
                    return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
                }
            }
            else if (TAG_EQ(name, "VAND"))
            {
                struct VAND *vand;

                if (xbv_push(fwinfo, &stack, xbv_firmware, xbv_vers, xbv_vand, list_end) ||
                    (fwinfo->vers.num_vand >= MAX_VAND))
                {
                    return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
                }

                /* Get a new VAND */
                vand = fwinfo->vand + fwinfo->vers.num_vand++;

                /* Fill it in */
                vand->first = fwinfo->num_vmeq;
                vand->count = 0;
            }
            else if (TAG_EQ(name, "PTCH"))
            {
                if (xbv_push(fwinfo, &stack, xbv_patch, xbv_xbv1, xbv_ptch, list_end))
                {
                    return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
                }
            }
            else
            {
                /* Skip over any other lists.  We dont bother to push
                 * the new list type now as we would only pop it at
                 * the end of the outer loop. */
                ct.ioffset += tag.t_len - 4;
            }
        }
        else if (TAG_EQ(tag.t_name, "SLTP"))
        {
            u32 addr;

            if (xbv_check(fwinfo, &stack, xbv_firmware, xbv_fw) ||
                (tag.t_len != 4) ||
                (fwinfo->slut_addr != 0) ||
                read_uint(card, &ct, &addr, 4))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            fwinfo->slut_addr = addr;
        }
        else if (TAG_EQ(tag.t_name, "FWDL"))
        {
            u32 addr;
            struct FWDL *fwdl;

            if (xbv_check(fwinfo, &stack, xbv_firmware, xbv_fw) ||
                (fwinfo->num_fwdl >= MAX_FWDL) ||
                (read_uint(card, &ct, &addr, 4)))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            fwdl = fwinfo->fwdl + fwinfo->num_fwdl++;

            fwdl->dl_size = tag.t_len - 4;
            fwdl->dl_addr = addr;
            fwdl->dl_offset = ct.ioffset;

            ct.ioffset += tag.t_len - 4;
        }
        else if (TAG_EQ(tag.t_name, "FWOV"))
        {
            if (xbv_check(fwinfo, &stack, xbv_firmware, xbv_fw) ||
                (fwinfo->fwov.dl_size != 0) ||
                (fwinfo->fwov.dl_offset != 0))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            fwinfo->fwov.dl_size = tag.t_len;
            fwinfo->fwov.dl_offset = ct.ioffset;

            ct.ioffset += tag.t_len;
        }
        else if (TAG_EQ(tag.t_name, "VMEQ"))
        {
            u32 temp[3];
            struct VAND *vand;
            struct VMEQ *vmeq;

            if (xbv_check(fwinfo, &stack, xbv_firmware, xbv_vand) ||
                (fwinfo->num_vmeq >= MAX_VMEQ) ||
                (fwinfo->vers.num_vand == 0) ||
                (tag.t_len != 8) ||
                read_uint(card, &ct, &temp[0], 4) ||
                read_uint(card, &ct, &temp[1], 2) ||
                read_uint(card, &ct, &temp[2], 2))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            /* Get the last VAND */
            vand = fwinfo->vand + (fwinfo->vers.num_vand - 1);

            /* Get a new VMEQ */
            vmeq = fwinfo->vmeq + fwinfo->num_vmeq++;

            /* Note that this VAND contains another VMEQ */
            vand->count++;

            /* Fill in the VMEQ */
            vmeq->addr = temp[0];
            vmeq->mask = (u16)temp[1];
            vmeq->value = (u16)temp[2];
        }
        else if (TAG_EQ(tag.t_name, "FWID"))
        {
            u32 build_id;

            if (xbv_check(fwinfo, &stack, xbv_patch, xbv_ptch) ||
                (tag.t_len != 4) ||
                (fwinfo->build_id != 0) ||
                read_uint(card, &ct, &build_id, 4))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            fwinfo->build_id = build_id;
        }
        else if (TAG_EQ(tag.t_name, "PTDL"))
        {
            struct PTDL *ptdl;

            if (xbv_check(fwinfo, &stack, xbv_patch, xbv_ptch) ||
                (fwinfo->num_ptdl >= MAX_PTDL))
            {
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }

            /* Allocate a new PTDL */
            ptdl = fwinfo->ptdl + fwinfo->num_ptdl++;

            ptdl->dl_size = tag.t_len;
            ptdl->dl_offset = ct.ioffset;

            ct.ioffset += tag.t_len;
        }
        else
        {
            /*
             * If we get here it is a tag we are not interested in,
             * just skip over it.
             */
            ct.ioffset += tag.t_len;
        }

        /* Check to see if we are at the end of the currently stacked
         * segment.  We could finish more than one list at a time. */
        while (ct.ioffset >= stack.s[stack.ptr].ioffset_end)
        {
            if (ct.ioffset > stack.s[stack.ptr].ioffset_end)
            {
                unifi_error(NULL,
                            "XBV file has overrun stack'd segment %d (%d > %d)\n",
                            stack.ptr, ct.ioffset, stack.s[stack.ptr].ioffset_end);
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }
            if (stack.ptr <= 0)
            {
                unifi_error(NULL, "XBV file has underrun stack pointer\n");
                return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
            }
            stack.ptr--;
        }
    }

    if (stack.ptr != 0)
    {
        unifi_error(NULL, "Last list of XBV is not complete.\n");
        return CSR_WIFI_HIP_RESULT_INVALID_VALUE;
    }

    return CSR_RESULT_SUCCESS;
} /* xbv1_parse() */


/* Check the the XBV file is of a consistant sort (either firmware or
 * patch) and that we are in the correct containing list type. */
static s32 xbv_check(xbv1_t *fwinfo, const xbv_stack_t *stack,
                          xbv_mode new_mode, xbv_container old_cont)
{
    /* If the new file mode is unknown the current packet could be in
     * either (any) type of XBV file, and we cant make a decission at
     * this time. */
    if (new_mode != xbv_unknown)
    {
        if (fwinfo->mode == xbv_unknown)
        {
            fwinfo->mode = new_mode;
        }
        else if (fwinfo->mode != new_mode)
        {
            return -1;
        }
    }
    /* If the current stack top doesn't match what we expect then the
     * file is corrupt. */
    if (stack->s[stack->ptr].container != old_cont)
    {
        return -1;
    }
    return 0;
}


/* Make checks as above and then enter a new list */
static s32 xbv_push(xbv1_t *fwinfo, xbv_stack_t *stack,
                         xbv_mode new_mode, xbv_container old_cont,
                         xbv_container new_cont, u32 new_ioff)
{
    if (xbv_check(fwinfo, stack, new_mode, old_cont))
    {
        return -1;
    }

    /* Check that our stack won't overflow. */
    if (stack->ptr >= (XBV_STACK_SIZE - 1))
    {
        return -1;
    }

    /* Add the new list element to the top of the stack. */
    stack->ptr++;
    stack->s[stack->ptr].container = new_cont;
    stack->s[stack->ptr].ioffset_end = new_ioff;

    return 0;
}


static u32 xbv2uint(u8 *ptr, s32 len)
{
    u32 u = 0;
    s16 i;

    for (i = 0; i < len; i++)
    {
        u32 b;
        b = ptr[i];
        u += b << (i * 8);
    }
    return u;
}


static s32 read_tag(card_t *card, ct_t *ct, tag_t *tag)
{
    u8 buf[8];
    s32 n;

    n = (*ct->iread)(card->ospriv, ct->dlpriv, ct->ioffset, buf, 8);
    if (n <= 0)
    {
        return n;
    }

    /* read the tag and length */
    if (n != 8)
    {
        return -1;
    }

    /* get section tag */
    memcpy(tag->t_name, buf, 4);

    /* get section length */
    tag->t_len = xbv2uint(buf + 4, 4);

    ct->ioffset += 8;

    return 8;
} /* read_tag() */


static s32 read_bytes(card_t *card, ct_t *ct, void *buf, u32 len)
{
    /* read the tag value */
    if ((*ct->iread)(card->ospriv, ct->dlpriv, ct->ioffset, buf, len) != (s32)len)
    {
        return -1;
    }

    ct->ioffset += len;

    return 0;
} /* read_bytes() */


static s32 read_uint(card_t *card, ct_t *ct, u32 *u, u32 len)
{
    u8 buf[4];

    /* Integer cannot be more than 4 bytes */
    if (len > 4)
    {
        return -1;
    }

    if (read_bytes(card, ct, buf, len))
    {
        return -1;
    }

    *u = xbv2uint(buf, len);

    return 0;
} /* read_uint() */


static u32 write_uint16(void *buf, const u32 offset, const u16 val)
{
    u8 *dst = (u8 *)buf + offset;
    *dst++ = (u8)(val & 0xff); /* LSB first */
    *dst = (u8)(val >> 8);
    return sizeof(u16);
}


static u32 write_uint32(void *buf, const u32 offset, const u32 val)
{
    (void)write_uint16(buf, offset + 0, (u16)(val & 0xffff));
    (void)write_uint16(buf, offset + 2, (u16)(val >> 16));
    return sizeof(u32);
}


static u32 write_bytes(void *buf, const u32 offset, const u8 *data, const u32 len)
{
    u32 i;
    u8 *dst = (u8 *)buf + offset;

    for (i = 0; i < len; i++)
    {
        *dst++ = *((u8 *)data + i);
    }
    return len;
}


static u32 write_tag(void *buf, const u32 offset, const char *tag_str)
{
    u8 *dst = (u8 *)buf + offset;
    memcpy(dst, tag_str, 4);
    return 4;
}


static u32 write_chunk(void *buf, const u32 offset, const char *tag_str, const u32 payload_len)
{
    u32 written = 0;
    written += write_tag(buf, offset, tag_str);
    written += write_uint32(buf, written + offset, (u32)payload_len);

    return written;
}


static u16 calc_checksum(void *buf, const u32 offset, const u32 bytes_len)
{
    u32 i;
    u8 *src = (u8 *)buf + offset;
    u16 sum = 0;
    u16 val;

    for (i = 0; i < bytes_len / 2; i++)
    {
        /* Contents copied to file is LE, host might not be */
        val = (u16) * src++;         /* LSB */
        val += (u16)(*src++) << 8;   /* MSB */
        sum += val;
    }

    /* Total of uint16s in the stream plus the stored check value
     * should equal STREAM_CHECKSUM when decoded.
     */
    return (STREAM_CHECKSUM - sum);
}


#define PTDL_RESET_DATA_SIZE  20  /* Size of reset vectors PTDL */

static u32 calc_patch_size(const xbv1_t *fwinfo)
{
    s16 i;
    u32 size = 0;

    /*
     * Work out how big an equivalent patch format file must be for this image.
     * This only needs to be approximate, so long as it's large enough.
     */
    if (fwinfo->mode != xbv_firmware)
    {
        return 0;
    }

    /* Payload (which will get put into a series of PTDLs) */
    for (i = 0; i < fwinfo->num_fwdl; i++)
    {
        size += fwinfo->fwdl[i].dl_size;
    }

    /* Another PTDL at the end containing reset vectors */
    size += PTDL_RESET_DATA_SIZE;

    /* PTDL headers. Add one for remainder, one for reset vectors */
    size += ((fwinfo->num_fwdl / PTDL_MAX_SIZE) + 2) * PTDL_HDR_SIZE;

    /* Another 1K sufficient to cover miscellaneous headers */
    size += 1024;

    return size;
}


static u32 write_xbv_header(void *buf, const u32 offset, const u32 file_payload_length)
{
    u32 written = 0;

    /* The length value given to the XBV chunk is the length of all subsequent
     * contents of the file, excluding the 8 byte size of the XBV1 header itself
     * (The added 6 bytes thus accounts for the size of the VERF)
     */
    written += write_chunk(buf, offset + written, (char *)"XBV1", file_payload_length + 6);

    written += write_chunk(buf, offset + written, (char *)"VERF", 2);
    written += write_uint16(buf,  offset + written, 0);      /* File version */

    return written;
}


static u32 write_ptch_header(void *buf, const u32 offset, const u32 fw_id)
{
    u32 written = 0;

    /* LIST is written with a zero length, to be updated later */
    written += write_chunk(buf, offset + written, (char *)"LIST", 0);
    written += write_tag(buf, offset + written, (char *)"PTCH");        /* List type */

    written += write_chunk(buf, offset + written, (char *)"FWID", 4);
    written += write_uint32(buf, offset + written, fw_id);


    return written;
}


#define UF_REGION_PHY  1
#define UF_REGION_MAC  2
#define UF_MEMPUT_MAC  0x0000
#define UF_MEMPUT_PHY  0x1000

static u32 write_patchcmd(void *buf, const u32 offset, const u32 dst_genaddr, const u16 len)
{
    u32 written = 0;
    u32 region = (dst_genaddr >> 28);
    u16 cmd_and_len = UF_MEMPUT_MAC;

    if (region == UF_REGION_PHY)
    {
        cmd_and_len = UF_MEMPUT_PHY;
    }
    else if (region != UF_REGION_MAC)
    {
        return 0; /* invalid */
    }

    /* Write the command and data length */
    cmd_and_len |= len;
    written += write_uint16(buf, offset + written, cmd_and_len);

    /* Write the destination generic address */
    written += write_uint16(buf, offset + written, (u16)(dst_genaddr >> 16));
    written += write_uint16(buf, offset + written, (u16)(dst_genaddr & 0xffff));

    /* The data payload should be appended to the command */
    return written;
}


static u32 write_fwdl_to_ptdl(void *buf, const u32 offset, fwreadfn_t readfn,
                                    const struct FWDL *fwdl, const void *dlpriv,
                                    const u32 fw_id, void *fw_buf)
{
    u32 written = 0;
    s16 chunks = 0;
    u32 left = fwdl->dl_size;      /* Bytes left in this fwdl */
    u32 dl_addr = fwdl->dl_addr;   /* Target address of fwdl image on XAP */
    u32 dl_offs = fwdl->dl_offset; /* Offset of fwdl image data in source */
    u16 csum;
    u32 csum_start_offs;           /* first offset to include in checksum */
    u32 sec_data_len;              /* section data byte count */
    u32 sec_len;                   /* section data + header byte count */

    /* FWDL maps to one or more PTDLs, as max size for a PTDL is 1K words */
    while (left)
    {
        /* Calculate amount to be transferred */
        sec_data_len = min_t(u32, left, PTDL_MAX_SIZE - PTDL_HDR_SIZE);
        sec_len = sec_data_len + PTDL_HDR_SIZE;

        /* Write PTDL header + entire PTDL size */
        written += write_chunk(buf, offset + written, (char *)"PTDL", sec_len);
        /* bug digest implies 4 bytes of padding here, but that seems wrong */

        /* Checksum starts here */
        csum_start_offs = offset + written;

        /* Patch-chunk header: fw_id. Note that this is in XAP word order */
        written += write_uint16(buf, offset + written, (u16)(fw_id >> 16));
        written += write_uint16(buf, offset + written, (u16)(fw_id & 0xffff));

        /* Patch-chunk header: section length in uint16s */
        written += write_uint16(buf, offset + written, (u16)(sec_len / 2));


        /* Write the appropriate patch command for the data's destination ptr */
        written += write_patchcmd(buf, offset + written, dl_addr, (u16)(sec_data_len / 2));

        /* Write the data itself (limited to the max chunk length) */
        if (readfn(NULL, (void *)dlpriv, dl_offs, fw_buf, sec_data_len) < 0)
        {
            return 0;
        }

        written += write_bytes(buf,
                               offset + written,
                               fw_buf,
                               sec_data_len);

        /* u16 checksum calculated over data written */
        csum = calc_checksum(buf, csum_start_offs, written - (csum_start_offs - offset));
        written += write_uint16(buf, offset + written, csum);

        left -= sec_data_len;
        dl_addr += sec_data_len;
        dl_offs += sec_data_len;
        chunks++;
    }

    return written;
}


#define SEC_CMD_LEN         ((4 + 2) * 2) /* sizeof(cmd, vector) per XAP */
#define PTDL_VEC_HDR_SIZE   (4 + 2 + 2)   /* sizeof(fw_id, sec_len, csum) */
#define UF_MAC_START_VEC    0x00c00000    /* Start address of image on MAC */
#define UF_PHY_START_VEC    0x00c00000    /* Start address of image on PHY */
#define UF_MAC_START_CMD    0x6000        /* MAC "Set start address" command */
#define UF_PHY_START_CMD    0x7000        /* PHY "Set start address" command */

static u32 write_reset_ptdl(void *buf, const u32 offset, const xbv1_t *fwinfo, u32 fw_id)
{
    u32 written = 0;
    u16 csum;
    u32 csum_start_offs;                 /* first offset to include in checksum */
    u32 sec_len;                         /* section data + header byte count */

    sec_len = SEC_CMD_LEN + PTDL_VEC_HDR_SIZE; /* Total section byte length */

    /* Write PTDL header + entire PTDL size */
    written += write_chunk(buf, offset + written, (char *)"PTDL", sec_len);

    /* Checksum starts here */
    csum_start_offs = offset + written;

    /* Patch-chunk header: fw_id. Note that this is in XAP word order */
    written += write_uint16(buf, offset + written, (u16)(fw_id >> 16));
    written += write_uint16(buf, offset + written, (u16)(fw_id & 0xffff));

    /* Patch-chunk header: section length in uint16s */
    written += write_uint16(buf, offset + written, (u16)(sec_len / 2));

    /*
     * Restart addresses to be executed on subsequent loader restart command.
     */

    /* Setup the MAC start address, note word ordering */
    written += write_uint16(buf, offset + written, UF_MAC_START_CMD);
    written += write_uint16(buf, offset + written, (UF_MAC_START_VEC >> 16));
    written += write_uint16(buf, offset + written, (UF_MAC_START_VEC & 0xffff));

    /* Setup the PHY start address, note word ordering */
    written += write_uint16(buf, offset + written, UF_PHY_START_CMD);
    written += write_uint16(buf, offset + written, (UF_PHY_START_VEC >> 16));
    written += write_uint16(buf, offset + written, (UF_PHY_START_VEC & 0xffff));

    /* u16 checksum calculated over data written */
    csum = calc_checksum(buf, csum_start_offs, written - (csum_start_offs - offset));
    written += write_uint16(buf, offset + written, csum);

    return written;
}


/*
 * ---------------------------------------------------------------------------
 *  read_slut
 *
 *      desc
 *
 *  Arguments:
 *      readfn          Pointer to function to call to read from the file.
 *      dlpriv          Opaque pointer arg to pass to readfn.
 *      addr            Offset into firmware image of SLUT.
 *      fwinfo          Pointer to fwinfo struct to fill in.
 *
 *  Returns:
 *      Number of SLUT entries in the f/w, or -1 if the image was corrupt.
 * ---------------------------------------------------------------------------
 */
s32 xbv1_read_slut(card_t *card, fwreadfn_t readfn, void *dlpriv, xbv1_t *fwinfo,
                        symbol_t *slut, u32 slut_len)
{
    s16 i;
    s32 offset;
    u32 magic;
    u32 count = 0;
    ct_t ct;

    if (fwinfo->mode != xbv_firmware)
    {
        return -1;
    }

    /* Find the d/l segment containing the SLUT */
    /* This relies on the SLUT being entirely contained in one segment */
    offset = -1;
    for (i = 0; i < fwinfo->num_fwdl; i++)
    {
        if ((fwinfo->slut_addr >= fwinfo->fwdl[i].dl_addr) &&
            (fwinfo->slut_addr < (fwinfo->fwdl[i].dl_addr + fwinfo->fwdl[i].dl_size)))
        {
            offset = fwinfo->fwdl[i].dl_offset +
                     (fwinfo->slut_addr - fwinfo->fwdl[i].dl_addr);
        }
    }
    if (offset < 0)
    {
        return -1;
    }

    ct.dlpriv = dlpriv;
    ct.ioffset = offset;
    ct.iread = readfn;

    if (read_uint(card, &ct, &magic, 2))
    {
        return -1;
    }
    if (magic != SLUT_FINGERPRINT)
    {
        return -1;
    }

    while (count < slut_len)
    {
        u32 id, obj;

        /* Read Symbol Id */
        if (read_uint(card, &ct, &id, 2))
        {
            return -1;
        }

        /* Check for end of table marker */
        if (id == CSR_SLT_END)
        {
            break;
        }

        /* Read Symbol Value */
        if (read_uint(card, &ct, &obj, 4))
        {
            return -1;
        }

        slut[count].id  = (u16)id;
        slut[count].obj = obj;
        count++;
    }

    return count;
} /* read_slut() */


/*
 * ---------------------------------------------------------------------------
 *  xbv_to_patch
 *
 *      Convert (the relevant parts of) a firmware xbv file into a patch xbv
 *
 *  Arguments:
 *      card
 *      fw_buf - pointer to xbv firmware image
 *      fwinfo - structure describing the firmware image
 *      size   - pointer to location into which size of f/w is written.
 *
 *  Returns:
 *      Pointer to firmware image, or NULL on error. Caller must free this
 *      buffer via kfree() once it's finished with.
 *
 *  Notes:
 *      The input fw_buf should have been checked via xbv1_parse prior to
 *      calling this function, so the input image is assumed valid.
 * ---------------------------------------------------------------------------
 */
#define PTCH_LIST_SIZE 16         /* sizeof PTCH+FWID chunk in LIST header */

void* xbv_to_patch(card_t *card, fwreadfn_t readfn,
                   const void *fw_buf, const xbv1_t *fwinfo, u32 *size)
{
    void *patch_buf = NULL;
    u32 patch_buf_size;
    u32 payload_offs = 0;           /* Start of XBV payload */
    s16 i;
    u32 patch_offs = 0;
    u32 list_len_offs = 0;          /* Offset of PTDL LIST length parameter */
    u32 ptdl_start_offs = 0;        /* Offset of first PTDL chunk */
    u32 fw_id;
    void *rdbuf;

    if (!fw_buf || !fwinfo || !card)
    {
        return NULL;
    }

    if (fwinfo->mode != xbv_firmware)
    {
        unifi_error(NULL, "Not a firmware file\n");
        return NULL;
    }

    /* Pre-allocate read buffer for chunk conversion */
    rdbuf = kmalloc(PTDL_MAX_SIZE, GFP_KERNEL);
    if (!rdbuf)
    {
        unifi_error(card, "Couldn't alloc conversion buffer\n");
        return NULL;
    }

    /* Loader requires patch file's build ID to match the running firmware's */
    fw_id = card->build_id;

    /* Firmware XBV1 contains VERF, optional INFO, SLUT(s), FWDL(s)          */
    /* Other chunks should get skipped.                                      */
    /* VERF should be sanity-checked against chip version                    */

    /* Patch    XBV1 contains VERF, optional INFO, PTCH                      */
    /*          PTCH contains FWID, optional INFO, PTDL(s), PTDL(start_vec)  */
    /* Each FWDL is split into PTDLs (each is 1024 XAP words max)            */
    /* Each PTDL contains running ROM f/w version, and checksum              */
    /* MAC/PHY reset addresses (known) are added into a final PTDL           */

    /* The input image has already been parsed, and loaded into fwinfo, so we
     * can use that to build the output image
     */
    patch_buf_size = calc_patch_size(fwinfo);

    patch_buf = kmalloc(patch_buf_size, GFP_KERNEL);
    if (!patch_buf)
    {
        kfree(rdbuf);
        unifi_error(NULL, "Can't malloc buffer for patch conversion\n");
        return NULL;
    }

    memset(patch_buf, 0xdd, patch_buf_size);

    /* Write XBV + VERF headers */
    patch_offs += write_xbv_header(patch_buf, patch_offs, 0);
    payload_offs = patch_offs;

    /* Write patch (LIST) header */
    list_len_offs = patch_offs + 4;    /* Save LIST.length offset for later update */
    patch_offs += write_ptch_header(patch_buf, patch_offs, fw_id);

    /* Save start offset of the PTDL chunks */
    ptdl_start_offs = patch_offs;

    /* Write LIST of firmware PTDL blocks */
    for (i = 0; i < fwinfo->num_fwdl; i++)
    {
        patch_offs += write_fwdl_to_ptdl(patch_buf,
                                         patch_offs,
                                         readfn,
                                         &fwinfo->fwdl[i],
                                         fw_buf,
                                         fw_id,
                                         rdbuf);
    }

    /* Write restart-vector PTDL last */
    patch_offs += write_reset_ptdl(patch_buf, patch_offs, fwinfo, fw_id);

    /* Now the length is known, update the LIST.length */
    (void)write_uint32(patch_buf, list_len_offs,
                       (patch_offs - ptdl_start_offs) + PTCH_LIST_SIZE);

    /* Re write XBV headers just to fill in the correct file size */
    (void)write_xbv_header(patch_buf, 0, (patch_offs - payload_offs));

    unifi_trace(card->ospriv, UDBG1, "XBV:PTCH size %u, fw_id %u\n",
                patch_offs, fw_id);
    if (size)
    {
        *size = patch_offs;
    }
    kfree(rdbuf);

    return patch_buf;
}


