/*
 ** pack.c - Array#pack, String#unpack
 */

#include <mruby.h>
#include "mruby/error.h"
#include "mruby/array.h"
#include "mruby/class.h"
#include "mruby/numeric.h"
#include "mruby/string.h"
#include "mruby/variable.h"
#include "mruby/endian.h"

#include <ctype.h>
#include <string.h>

#define INT_OVERFLOW_P(n)  ((n) < MRB_INT_MIN || (n) > MRB_INT_MAX)
#define UINT_OVERFLOW_P(n) ((n) > MRB_INT_MAX)

#ifndef EOF
# define EOF (-1) /* for MRB_NO_STDIO */
#endif

struct tmpl {
  mrb_value str;
  int idx;
};

enum pack_dir {
  PACK_DIR_CHAR,      /* C */
  PACK_DIR_SHORT,     /* S */
  PACK_DIR_LONG,      /* L */
  PACK_DIR_QUAD,      /* Q */
  //PACK_DIR_INT,     /* i */
  //PACK_DIR_VAX,
  PACK_DIR_BER,       /* w */
  PACK_DIR_UTF8,      /* U */
  PACK_DIR_DOUBLE,    /* E */
  PACK_DIR_FLOAT,     /* f */
  PACK_DIR_STR,       /* A */
  PACK_DIR_HEX,       /* h */
  PACK_DIR_BSTR,      /* b */
  PACK_DIR_BASE64,    /* m */
  PACK_DIR_QENC,      /* M */
  PACK_DIR_NUL,       /* x */
  PACK_DIR_BACK,      /* X */
  PACK_DIR_ABS,       /* @ */
  PACK_DIR_NONE,      /* - */
};

enum pack_type {
  PACK_TYPE_INTEGER,
  PACK_TYPE_FLOAT,
  PACK_TYPE_STRING,
  PACK_TYPE_NONE
};

#define PACK_FLAG_s             0x00000001      /* native size ("_" "!") */
#define PACK_FLAG_a             0x00000002      /* null padding ("a") */
#define PACK_FLAG_Z             0x00000004      /* append nul char ("z") */
#define PACK_FLAG_SIGNED        0x00000008      /* native size ("_" "!") */
#define PACK_FLAG_GT            0x00000010      /* big endian (">") */
#define PACK_FLAG_LT            0x00000020      /* little endian ("<") */
#define PACK_FLAG_WIDTH         0x00000040      /* "count" is "width" */
#define PACK_FLAG_LSB           0x00000080      /* LSB / low nibble first */
#define PACK_FLAG_COUNT2        0x00000100      /* "count" is special... */
#define PACK_FLAG_LITTLEENDIAN  0x00000200      /* little endian actually */

#define PACK_BASE64_IGNORE      0xff
#define PACK_BASE64_PADDING     0xfe

const static unsigned char base64chars[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const static unsigned char base64_dec_tab[] =
  "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
  "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
  "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x3e\xff\xff\xff\x3f"
  "\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\xff\xff\xff\xfe\xff\xff"
  "\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e"
  "\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\xff\xff\xff\xff\xff"
  "\xff\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28"
  "\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\xff\xff\xff\xff\xff";

static int
hex2int(unsigned char ch)
{
  if (ch >= '0' && ch <= '9')
    return ch - '0';
  else if (ch >= 'A' && ch <= 'F')
    return 10 + (ch - 'A');
  else if (ch >= 'a' && ch <= 'f')
    return 10 + (ch - 'a');
  else
    return -1;
}

static mrb_value
str_len_ensure(mrb_state *mrb, mrb_value str, mrb_int len)
{
  mrb_int n = RSTRING_LEN(str);
  if (len < 0) {
    mrb_raise(mrb, E_RANGE_ERROR, "negative (or overflowed) integer");
  }
  if (len > n) {
    do {
      n *= 2;
    } while (len > n);
    str = mrb_str_resize(mrb, str, n);
  }
  return str;
}


static int
pack_char(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  str = str_len_ensure(mrb, str, sidx + 1);
  RSTRING_PTR(str)[sidx] = (char)mrb_integer(o);
  return 1;
}

static int
unpack_char(mrb_state *mrb, const void *src, int srclen, mrb_value ary, unsigned int flags)
{
  if (flags & PACK_FLAG_SIGNED)
    mrb_ary_push(mrb, ary, mrb_fixnum_value(*(signed char*)src));
  else
    mrb_ary_push(mrb, ary, mrb_fixnum_value(*(unsigned char*)src));
  return 1;
}

static int
pack_short(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  uint16_t n;

  str = str_len_ensure(mrb, str, sidx + 2);
  n = (uint16_t)mrb_integer(o);
  if (flags & PACK_FLAG_LITTLEENDIAN) {
    RSTRING_PTR(str)[sidx+0] = n % 256;
    RSTRING_PTR(str)[sidx+1] = n / 256;
  }
  else {
    RSTRING_PTR(str)[sidx+0] = n / 256;
    RSTRING_PTR(str)[sidx+1] = n % 256;
  }
  return 2;
}

static int
unpack_short(mrb_state *mrb, const unsigned char *src, int srclen, mrb_value ary, unsigned int flags)
{
  int n;

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    n = src[1] * 256 + src[0];
  }
  else {
    n = src[0] * 256 + src[1];
  }
  if ((flags & PACK_FLAG_SIGNED) && (n >= 0x8000)) {
    n -= 0x10000;
  }
  mrb_ary_push(mrb, ary, mrb_fixnum_value(n));
  return 2;
}

static int
pack_long(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  uint32_t n;

  str = str_len_ensure(mrb, str, sidx + 4);
  n = (uint32_t)mrb_integer(o);
  if (flags & PACK_FLAG_LITTLEENDIAN) {
    RSTRING_PTR(str)[sidx+0] = (char)(n & 0xff);
    RSTRING_PTR(str)[sidx+1] = (char)(n >> 8);
    RSTRING_PTR(str)[sidx+2] = (char)(n >> 16);
    RSTRING_PTR(str)[sidx+3] = (char)(n >> 24);
  }
  else {
    RSTRING_PTR(str)[sidx+0] = (char)(n >> 24);
    RSTRING_PTR(str)[sidx+1] = (char)(n >> 16);
    RSTRING_PTR(str)[sidx+2] = (char)(n >> 8);
    RSTRING_PTR(str)[sidx+3] = (char)(n & 0xff);
  }
  return 4;
}

#ifndef MRB_INT64
static void
u32tostr(char *buf, size_t len, uint32_t n)
{
#ifdef MRB_NO_STDIO
  char *bufend = buf + len;
  char *p = bufend - 1;

  if (len < 1) {
    return;
  }

  *p-- = '\0';
  len--;

  if (n > 0) {
    for (; len > 0 && n > 0; len --, n /= 10) {
      *p -- = '0' + (n % 10);
    }
    p++;
  }
  else if (len > 0) {
    *p = '0';
    len--;
  }

  memmove(buf, p, bufend - p);
#else
  snprintf(buf, len, "%" PRIu32, n);
#endif /* MRB_NO_STDIO */
}
#endif /* MRB_INT64 */

static int
unpack_long(mrb_state *mrb, const unsigned char *src, int srclen, mrb_value ary, unsigned int flags)
{
#ifndef MRB_INT64
  char msg[60];
#endif
  uint32_t ul;
  mrb_int n;

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    ul = (uint32_t)src[3] * 256*256*256;
    ul += (uint32_t)src[2] *256*256;
    ul += (uint32_t)src[1] *256;
    ul += (uint32_t)src[0];
  }
  else {
    ul = (uint32_t)src[0] * 256*256*256;
    ul += (uint32_t)src[1] *256*256;
    ul += (uint32_t)src[2] *256;
    ul += (uint32_t)src[3];
  }
  if (flags & PACK_FLAG_SIGNED) {
    n = (int32_t)ul;
  }
  else {
#ifndef MRB_INT64
    if (UINT_OVERFLOW_P(ul)) {
      u32tostr(msg, sizeof(msg), ul);
      mrb_raisef(mrb, E_RANGE_ERROR, "cannot unpack to Integer: %s", msg);
    }
#endif
    n = ul;
  }
  mrb_ary_push(mrb, ary, mrb_int_value(mrb, n));
  return 4;
}

static int
pack_quad(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  uint64_t n;

  str = str_len_ensure(mrb, str, sidx + 8);
  n = (uint64_t)mrb_integer(o);
  if (flags & PACK_FLAG_LITTLEENDIAN) {
    RSTRING_PTR(str)[sidx+0] = (char)(n & 0xff);
    RSTRING_PTR(str)[sidx+1] = (char)(n >> 8);
    RSTRING_PTR(str)[sidx+2] = (char)(n >> 16);
    RSTRING_PTR(str)[sidx+3] = (char)(n >> 24);
    RSTRING_PTR(str)[sidx+4] = (char)(n >> 32);
    RSTRING_PTR(str)[sidx+5] = (char)(n >> 40);
    RSTRING_PTR(str)[sidx+6] = (char)(n >> 48);
    RSTRING_PTR(str)[sidx+7] = (char)(n >> 56);
  }
  else {
    RSTRING_PTR(str)[sidx+0] = (char)(n >> 56);
    RSTRING_PTR(str)[sidx+1] = (char)(n >> 48);
    RSTRING_PTR(str)[sidx+2] = (char)(n >> 40);
    RSTRING_PTR(str)[sidx+3] = (char)(n >> 32);
    RSTRING_PTR(str)[sidx+4] = (char)(n >> 24);
    RSTRING_PTR(str)[sidx+5] = (char)(n >> 16);
    RSTRING_PTR(str)[sidx+6] = (char)(n >> 8);
    RSTRING_PTR(str)[sidx+7] = (char)(n & 0xff);
  }
  return 8;
}

static void
u64tostr(char *buf, size_t len, uint64_t n)
{
#ifdef MRB_NO_STDIO
  char *bufend = buf + len;
  char *p = bufend - 1;

  if (len < 1) {
    return;
  }

  *p-- = '\0';
  len--;

  if (n > 0) {
    for (; len > 0 && n > 0; len--, n /= 10) {
      *p-- = '0' + (n % 10);
    }
    p++;
  }
  else if (len > 0) {
    *p = '0';
    len--;
  }

  memmove(buf, p, bufend - p);
#else
  snprintf(buf, len, "%" PRIu64, n);
#endif /* MRB_NO_STDIO */
}

#ifndef MRB_INT64
static void
i64tostr(char *buf, size_t len, int64_t n)
{
#ifdef MRB_NO_STDIO
  if (len < 1) {
    return;
  }

  if (n < 0) {
    *buf++ = '-';
    len--;
    n = -n;
  }

  u64tostr(buf, len, (uint64_t)n);
#else
  snprintf(buf, len, "%" PRId64, n);
#endif /* MRB_NO_STDIO */
}
#endif /* MRB_INT64 */

static int
unpack_quad(mrb_state *mrb, const unsigned char *src, int srclen, mrb_value ary, unsigned int flags)
{
  char msg[60];
  uint64_t ull;
  int i, pos, step;
  mrb_int n;

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    pos  = 7;
    step = -1;
  }
  else {
    pos  = 0;
    step = 1;
  }
  ull = 0;
  for (i = 0; i < 8; i++) {
    ull = ull * 256 + (uint64_t)src[pos];
    pos += step;
  }
  if (flags & PACK_FLAG_SIGNED) {
    int64_t sll = ull;
#ifndef MRB_INT64
    if (INT_OVERFLOW_P(sll)) {
      i64tostr(msg, sizeof(msg), sll);
      mrb_raisef(mrb, E_RANGE_ERROR, "cannot unpack to Integer: %s", msg);
    }
#endif
    n = (mrb_int)sll;
  }
  else {
    if (UINT_OVERFLOW_P(ull)) {
      u64tostr(msg, sizeof(msg), ull);
      mrb_raisef(mrb, E_RANGE_ERROR, "cannot unpack to Integer: %s", msg);
    }
    n = (mrb_int)ull;
  }
  mrb_ary_push(mrb, ary, mrb_int_value(mrb, n));
  return 8;
}

static int
pack_BER(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  mrb_int n = mrb_integer(o);
  int i;
  char *p;

  if (n < 0) {
    mrb_raise(mrb, E_ARGUMENT_ERROR, "can't compress negative numbers");
  }
  for (i=1; i<(int)sizeof(mrb_int)+1; i++) {
    mrb_int mask = ~((1L<<(7*i))-1);
    if ((n & mask) == 0) break;
  }
  str = str_len_ensure(mrb, str, sidx + i);
  p = RSTRING_PTR(str)+sidx;
  for (size_t j=i; j>0; p++,j--) {
    mrb_int x = (n>>(7*(j-1)))&0x7f;
    *p = (char)x;
    if (j > 1) *p |= 0x80;
  }
  return i;
}

static int
unpack_BER(mrb_state *mrb, const unsigned char *src, int srclen, mrb_value ary, unsigned int flags)
{
  int i;
  mrb_int n = 0;
  const unsigned char *p = src;
  const unsigned char *e = p + srclen;

  if (srclen == 0) return 0;
  for (i=1; p<e; p++,i++) {
    if (n > (MRB_INT_MAX>>7)) {
      mrb_raise(mrb, E_RANGE_ERROR, "BER unpacking 'w' overflow");
    }
    n <<= 7;
    n |= *p & 0x7f;
    if ((*p & 0x80) == 0) break;
  }
  mrb_ary_push(mrb, ary, mrb_int_value(mrb, n));
  return i;
}

#ifndef MRB_NO_FLOAT
static int
pack_double(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  int i;
  double d;
  uint8_t *buffer = (uint8_t*)&d;
  str = str_len_ensure(mrb, str, sidx + 8);
  d = mrb_float(o);

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    if (littleendian) {
      memcpy(RSTRING_PTR(str) + sidx, buffer, 8);
    }
    else {
      for (i = 0; i < 8; i++) {
        RSTRING_PTR(str)[sidx + i] = buffer[8 - i - 1];
      }
    }
  }
  else {
    if (littleendian) {
      for (i = 0; i < 8; i++) {
        RSTRING_PTR(str)[sidx + i] = buffer[8 - i - 1];
      }
    }
    else {
      memcpy(RSTRING_PTR(str) + sidx, buffer, 8);
    }
  }

  return 8;
}

static int
unpack_double(mrb_state *mrb, const unsigned char * src, int srclen, mrb_value ary, unsigned int flags)
{
  int i;
  double d;
  uint8_t *buffer = (uint8_t*)&d;

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    if (littleendian) {
      memcpy(buffer, src, 8);
    }
    else {
      for (i = 0; i < 8; i++) {
        buffer[8 - i - 1] = src[i];
      }
    }
  }
  else {
    if (littleendian) {
      for (i = 0; i < 8; i++) {
        buffer[8 - i - 1] = src[i];
      }
    }
    else {
      memcpy(buffer, src, 8);
    }
  }
  mrb_ary_push(mrb, ary, mrb_float_value(mrb, d));

  return 8;
}

static int
pack_float(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, unsigned int flags)
{
  int i;
  float f;
  uint8_t *buffer = (uint8_t*)&f;
  str = str_len_ensure(mrb, str, sidx + 4);
  f = (float)mrb_float(o);

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    if (littleendian) {
      memcpy(RSTRING_PTR(str) + sidx, buffer, 4);
    }
    else {
      for (i = 0; i < 4; i++) {
        RSTRING_PTR(str)[sidx + i] = buffer[4 - i - 1];
      }
    }
  }
  else {
    if (littleendian) {
      for (i = 0; i < 4; i++) {
        RSTRING_PTR(str)[sidx + i] = buffer[4 - i - 1];
      }
    }
    else {
      memcpy(RSTRING_PTR(str) + sidx, buffer, 4);
    }
  }

  return 4;
}

static int
unpack_float(mrb_state *mrb, const unsigned char * src, int srclen, mrb_value ary, unsigned int flags)
{
  int i;
  float f;
  uint8_t *buffer = (uint8_t*)&f;

  if (flags & PACK_FLAG_LITTLEENDIAN) {
    if (littleendian) {
      memcpy(buffer, src, 4);
    }
    else {
      for (i = 0; i < 4; i++) {
        buffer[4 - i - 1] = src[i];
      }
    }
  }
  else {
    if (littleendian) {
      for (i = 0; i < 4; i++) {
        buffer[4 - i - 1] = src[i];
      }
    }
    else {
      memcpy(buffer, src, 4);
    }
  }
  mrb_ary_push(mrb, ary, mrb_float_value(mrb, f));

  return 4;
}
#endif

static int
pack_utf8(mrb_state *mrb, mrb_value o, mrb_value str, mrb_int sidx, int count, unsigned int flags)
{
  char utf8[4];
  int len = 0;
  uint32_t c = 0;

  c = (uint32_t)mrb_integer(o);

  /* Unicode character */
  /* from mruby-compiler gem */
  if (c < 0x80) {
    utf8[0] = (char)c;
    len = 1;
  }
  else if (c < 0x800) {
    utf8[0] = (char)(0xC0 | (c >> 6));
    utf8[1] = (char)(0x80 | (c & 0x3F));
    len = 2;
  }
  else if (c < 0x10000) {
    utf8[0] = (char)(0xE0 |  (c >> 12)        );
    utf8[1] = (char)(0x80 | ((c >>  6) & 0x3F));
    utf8[2] = (char)(0x80 | ( c        & 0x3F));
    len = 3;
  }
  else if (c < 0x200000) {
    utf8[0] = (char)(0xF0 |  (c >> 18)        );
    utf8[1] = (char)(0x80 | ((c >> 12) & 0x3F));
    utf8[2] = (char)(0x80 | ((c >>  6) & 0x3F));
    utf8[3] = (char)(0x80 | ( c        & 0x3F));
    len = 4;
  }
  else {
    mrb_raise(mrb, E_RANGE_ERROR, "pack(U): value out of range");
  }

  str = str_len_ensure(mrb, str, sidx + len);
  memcpy(RSTRING_PTR(str) + sidx, utf8, len);

  return len;
}

static const unsigned long utf8_limits[] = {
  0x0,        /* 1 */
  0x80,       /* 2 */
  0x800,      /* 3 */
  0x10000,    /* 4 */
  0x200000,   /* 5 */
  0x4000000,  /* 6 */
  0x80000000, /* 7 */
};

static unsigned long
utf8_to_uv(mrb_state *mrb, const char *p, long *lenp)
{
  int c = *p++ & 0xff;
  unsigned long uv = c;
  long n = 1;

  if (!(uv & 0x80)) {
    *lenp = 1;
    return uv;
  }
  if (!(uv & 0x40)) {
    *lenp = 1;
  malformed:
    mrb_raise(mrb, E_ARGUMENT_ERROR, "malformed UTF-8 character");
  }

  if      (!(uv & 0x20)) { n = 2; uv &= 0x1f; }
  else if (!(uv & 0x10)) { n = 3; uv &= 0x0f; }
  else if (!(uv & 0x08)) { n = 4; uv &= 0x07; }
  else if (!(uv & 0x04)) { n = 5; uv &= 0x03; }
  else if (!(uv & 0x02)) { n = 6; uv &= 0x01; }
  else {
    *lenp = 1;
    goto malformed;
  }
  if (n > *lenp) {
    mrb_raisef(mrb, E_ARGUMENT_ERROR, "malformed UTF-8 character (expected %d bytes, given %d bytes)",
               n, *lenp);
  }
  *lenp = n--;
  if (n != 0) {
    while (n--) {
      c = *p++ & 0xff;
      if ((c & 0xc0) != 0x80) {
        *lenp -= n + 1;
        goto malformed;
      }
      else {
        c &= 0x3f;
        uv = uv << 6 | c;
      }
    }
  }
  n = *lenp - 1;
  if (uv < utf8_limits[n]) {
    mrb_raise(mrb, E_ARGUMENT_ERROR, "redundant UTF-8 sequence");
  }
  return uv;
}

static int
unpack_utf8(mrb_state *mrb, const unsigned char * src, int srclen, mrb_value ary, unsigned int flags)
{
  unsigned long uv;
  long lenp = srclen;

  if (srclen == 0) {
    return 1;
  }
  uv = utf8_to_uv(mrb, (const char*)src, &lenp);
  mrb_ary_push(mrb, ary, mrb_fixnum_value((mrb_int)uv));
  return (int)lenp;
}

static int
pack_str(mrb_state *mrb, mrb_value src, mrb_value dst, mrb_int didx, int count, unsigned int flags)
{
  mrb_int copylen, slen, padlen;
  char *dptr, *dptr0, pad, *sptr;

  sptr = RSTRING_PTR(src);
  slen = RSTRING_LEN(src);

  if ((flags & PACK_FLAG_a) || (flags & PACK_FLAG_Z))
    pad = '\0';
  else
    pad = ' ';

  if (count == 0) {
    return 0;
  }
  else if (count == -1) {
    copylen = slen;
    padlen = (flags & PACK_FLAG_Z) ? 1 : 0;
  }
  else if (count < slen) {
    copylen = count;
    padlen = 0;
  }
  else {
    copylen = slen;
    padlen = count - slen;
  }

  dst = str_len_ensure(mrb, dst, didx + copylen + padlen);
  dptr0 = dptr = RSTRING_PTR(dst) + didx;
  memcpy(dptr, sptr, copylen);
  dptr += copylen;
  while (padlen-- > 0) {
    *dptr++ = pad;
  }

  return (int)(dptr - dptr0);
}

#define CHECK_UNPACK_LEN(mrb, slen, ary) do {\
  if ((slen) <= 0) {\
    mrb_ary_push(mrb, ary, mrb_str_new(mrb, 0, 0));\
    return 0;\
  }\
} while (0)

static int
unpack_str(mrb_state *mrb, const void *src, int slen, mrb_value ary, int count, unsigned int flags)
{
  CHECK_UNPACK_LEN(mrb, slen, ary);

  mrb_value dst;
  const char *cp, *sptr;
  int copylen;

  sptr = (const char*)src;
  if (count != -1 && count < slen)  {
    slen = count;
  }
  copylen = slen;

  if (slen >= 0 && flags & PACK_FLAG_Z) {  /* "Z" */
    if ((cp = (const char*)memchr(sptr, '\0', slen)) != NULL) {
      copylen = (int)(cp - sptr);
      if (count == -1) {
        slen = copylen + 1;
      }
    }
  }
  else if (!(flags & PACK_FLAG_a)) {  /* "A" */
    while (copylen > 0 && (sptr[copylen - 1] == '\0' || ISSPACE(sptr[copylen - 1]))) {
      copylen--;
    }
  }

  if (copylen < 0) copylen = 0;
  dst = mrb_str_new(mrb, sptr, (mrb_int)copylen);
  mrb_ary_push(mrb, ary, dst);
  return slen;
}


static int
pack_hex(mrb_state *mrb, mrb_value src, mrb_value dst, mrb_int didx, int count, unsigned int flags)
{
  int a, b;
  unsigned int ashift, bshift;
  long slen;
  char *dptr, *dptr0, *sptr;

  sptr = RSTRING_PTR(src);
  slen = (long)RSTRING_LEN(src);

  if (flags & PACK_FLAG_LSB) {
    ashift = 0;
    bshift = 4;
  }
  else {
    ashift = 4;
    bshift = 0;
  }

  if (count == -1) {
    count = slen;
  }
  else if (slen > count) {
    slen = count;
  }

  dst = str_len_ensure(mrb, dst, didx + count);
  dptr = RSTRING_PTR(dst) + didx;

  dptr0 = dptr;
  for (; count > 0; count -= 2) {
    a = b = 0;
    if (slen > 0) {
      a = hex2int(*sptr++);
      if (a < 0) break;
      slen--;
    }
    if (slen > 0) {
      b = hex2int(*sptr++);
      if (b < 0) break;
      slen--;
    }
    *dptr++ = (a << ashift) + (b << bshift);
  }

  return (int)(dptr - dptr0);
}

static int
unpack_hex(mrb_state *mrb, const void *src, int slen, mrb_value ary, int count, unsigned int flags)
{
  CHECK_UNPACK_LEN(mrb, slen, ary);

  mrb_value dst;
  int a, ashift, b, bshift;
  const char *sptr, *sptr0;
  char *dptr, *dptr0;
  const char hexadecimal[] = "0123456789abcdef";

  if (flags & PACK_FLAG_LSB) {
    ashift = 0;
    bshift = 4;
  }
  else {
    ashift = 4;
    bshift = 0;
  }

  sptr = (const char*)src;

  if (count == -1)
    count = slen * 2;

  dst = mrb_str_new(mrb, NULL, count);
  dptr = RSTRING_PTR(dst);

  sptr0 = sptr;
  dptr0 = dptr;
  while (slen > 0 && count > 0) {
    a = (*sptr >> ashift) & 0x0f;
    b = (*sptr >> bshift) & 0x0f;
    sptr++;
    slen--;

    *dptr++ = hexadecimal[a];
    count--;

    if (count > 0) {
      *dptr++ = hexadecimal[b];
      count--;
    }
  }

  dst = mrb_str_resize(mrb, dst, (mrb_int)(dptr - dptr0));
  mrb_ary_push(mrb, ary, dst);
  return (int)(sptr - sptr0);
}

static int
pack_bstr(mrb_state *mrb, mrb_value src, mrb_value dst, mrb_int didx, int count, unsigned int flags)
{
  const char *sptr = RSTRING_PTR(src);
  int slen = (int)RSTRING_LEN(src);

  if (count == -1) {
    count = slen;
  }
  else if (slen > count) {
    slen = count;
  }

  dst = str_len_ensure(mrb, dst, didx + count);
  char *dptr = RSTRING_PTR(dst) + didx;
  char *dptr0 = dptr;

  unsigned int byte = 0;
  for (int i=0; i++ < slen; sptr++) {
    if (flags & PACK_FLAG_LSB) {
      if (*sptr & 1)
        byte |= 128;
      if (i & 7)
        byte >>= 1;
      else {
        char c = (char)(byte&0xff);
        *dptr++ = c;
        byte = 0;
      }
    }
    else {
      byte |= *sptr & 1;
      if (i & 7)
        byte <<= 1;
      else {
        char c = (char)(byte&0xff);
        *dptr++ = c;
        byte = 0;
      }
    }
  }
  if (slen & 7) {
    if (flags & PACK_FLAG_LSB) {
      byte >>= 7 - (slen & 7);
    }
    else {
      byte <<= 7 - (slen & 7);
    }
    char c = (char)(byte&0xff);
    *dptr++ = c;
  }
  return (int)(dptr - dptr0);
}

static int
unpack_bstr(mrb_state *mrb, const void *src, int slen, mrb_value ary, int count, unsigned int flags)
{
  CHECK_UNPACK_LEN(mrb, slen, ary);

  const char *sptr0 = (const char*)src;
  const char *sptr = sptr0;
  if (count == -1)
    count = slen * 8;

  mrb_value dst = mrb_str_new(mrb, NULL, count);
  char *dptr = RSTRING_PTR(dst);
  const char *dptr0 = dptr;
  int bits = 0;

  for (int i=0; i<count; i++) {
    if (flags & PACK_FLAG_LSB) {
      if (i & 7) bits >>= 1;
      else bits = (unsigned char)*sptr++;
      *dptr++ = (bits & 1) ? '1' : '0';
    }
    else {
      if (i & 7) bits <<= 1;
      else bits = (unsigned char)*sptr++;
      *dptr++ = (bits & 128) ? '1' : '0';
    }
  }
  dst = mrb_str_resize(mrb, dst, (mrb_int)(dptr - dptr0));
  mrb_ary_push(mrb, ary, dst);
  return (int)(sptr - sptr0);
}

static int
pack_base64(mrb_state *mrb, mrb_value src, mrb_value dst, mrb_int didx, int count)
{
  mrb_int dstlen;
  unsigned long l;
  mrb_int column, srclen;
  char *srcptr, *dstptr, *dstptr0;

  srcptr = RSTRING_PTR(src);
  srclen = RSTRING_LEN(src);

  if (srclen == 0)  /* easy case */
    return 0;

  if (count != 0 && count < 3) {  /* -1, 1 or 2 */
    count = 45;
  }
  else if (count >= 3) {
    count -= count % 3;
  }

  dstlen = (srclen+2) / 3 * 4;
  if (count > 0) {
    dstlen += (srclen / count) + ((srclen % count) == 0 ? 0 : 1);
  }
  dst = str_len_ensure(mrb, dst, didx + dstlen);
  dstptr = RSTRING_PTR(dst) + didx;

  dstptr0 = dstptr;
  for (column = 3; srclen >= 3; srclen -= 3, column += 3) {
    l = (unsigned char)*srcptr++ << 16;
    l += (unsigned char)*srcptr++ << 8;
    l += (unsigned char)*srcptr++;

    *dstptr++ = base64chars[(l >> 18) & 0x3f];
    *dstptr++ = base64chars[(l >> 12) & 0x3f];
    *dstptr++ = base64chars[(l >>  6) & 0x3f];
    *dstptr++ = base64chars[ l        & 0x3f];

    if (column == count) {
      *dstptr++ = '\n';
      column = 0;
    }
  }
  if (srclen == 1) {
    l = (unsigned char)*srcptr++ << 16;
    *dstptr++ = base64chars[(l >> 18) & 0x3f];
    *dstptr++ = base64chars[(l >> 12) & 0x3f];
    *dstptr++ = '=';
    *dstptr++ = '=';
    column += 3;
  }
  else if (srclen == 2) {
    l = (unsigned char)*srcptr++ << 16;
    l += (unsigned char)*srcptr++ << 8;
    *dstptr++ = base64chars[(l >> 18) & 0x3f];
    *dstptr++ = base64chars[(l >> 12) & 0x3f];
    *dstptr++ = base64chars[(l >>  6) & 0x3f];
    *dstptr++ = '=';
    column += 3;
  }
  if (column > 0 && count > 0) {
    *dstptr++ = '\n';
  }

  return (int)(dstptr - dstptr0);
}

static int
unpack_base64(mrb_state *mrb, const void *src, int slen, mrb_value ary)
{
  CHECK_UNPACK_LEN(mrb, slen, ary);

  mrb_value dst;
  int dlen;
  unsigned long l;
  int i, padding;
  unsigned char c, ch[4];
  const char *sptr, *sptr0;
  char *dptr, *dptr0;

  sptr0 = sptr = (const char*)src;

  dlen = slen / 4 * 3;  /* an estimated value - may be shorter */
  dst = mrb_str_new(mrb, NULL, dlen);
  dptr0 = dptr = RSTRING_PTR(dst);

  padding = 0;
  while (slen >= 4) {
    for (i = 0; i < 4; i++) {
      do {
        if (slen-- == 0)
          goto done;
        c = *sptr++;
        if (c >= sizeof(base64_dec_tab))
          continue;
        ch[i] = base64_dec_tab[c];
        if (ch[i] == PACK_BASE64_PADDING) {
          ch[i] = 0;
          padding++;
        }
      } while (c >= sizeof(base64_dec_tab) || ch[i] == PACK_BASE64_IGNORE);
    }

    l = (ch[0] << 18) + (ch[1] << 12) + (ch[2] << 6) + ch[3];

    if (padding == 0) {
      *dptr++ = (l >> 16) & 0xff;
      *dptr++ = (l >> 8) & 0xff;
      *dptr++ = l & 0xff;
    }
    else if (padding == 1) {
      *dptr++ = (l >> 16) & 0xff;
      *dptr++ = (l >> 8) & 0xff;
      break;
    }
    else {
      *dptr++ = (l >> 16) & 0xff;
      break;
    }
  }

done:
  dst = mrb_str_resize(mrb, dst, (mrb_int)(dptr - dptr0));
  mrb_ary_push(mrb, ary, dst);
  return (int)(sptr - sptr0);
}

static int
pack_qenc(mrb_state *mrb, mrb_value src, mrb_value dst, mrb_int didx, int count)
{
  static const char hex_table[] = "0123456789ABCDEF";
  char buff[1024];
  char *s = RSTRING_PTR(src);
  char *send = s + RSTRING_LEN(src);
  int i = 0, n = 0, prev = EOF;
  int dlen = 0;

  if (count <= 1) count = 72;
  while (s < send) {
    if ((*s > 126) ||
        (*s < 32 && *s != '\n' && *s != '\t') ||
        (*s == '=')) {
      buff[i++] = '=';
      buff[i++] = hex_table[(*s & 0xf0) >> 4];
      buff[i++] = hex_table[*s & 0x0f];
      n += 3;
      prev = EOF;
    }
    else if (*s == '\n') {
      if (prev == ' ' || prev == '\t') {
        buff[i++] = '=';
        buff[i++] = *s;
      }
      buff[i++] = *s;
      n = 0;
      prev = *s;
    }
    else {
      buff[i++] = *s;
      n++;
      prev = *s;
    }
    if (n > count) {
      buff[i++] = '=';
      buff[i++] = '\n';
      n = 0;
      prev = '\n';
    }
    if (i > 1024 - 5) {
      str_len_ensure(mrb, dst, didx+dlen+i);
      memcpy(RSTRING_PTR(dst)+didx+dlen, buff, i);
      dlen += i;
      i = 0;
    }
    s++;
  }
  if (n > 0) {
    buff[i++] = '=';
    buff[i++] = '\n';
  }
  if (i > 0) {
    str_len_ensure(mrb, dst, didx+dlen+i);
    memcpy(RSTRING_PTR(dst)+didx+dlen, buff, i);
    dlen += i;
  }
  return dlen;
}

static int
unpack_qenc(mrb_state *mrb, const void *src, int slen, mrb_value ary)
{
  CHECK_UNPACK_LEN(mrb, slen, ary);

  mrb_value buf = mrb_str_new(mrb, 0, slen);
  const char *s = (const char*)src, *ss = s;
  const char *send = s + slen;
  char *ptr = RSTRING_PTR(buf);
  int c1, c2;

  while (s < send) {
    if (*s == '=') {
      if (++s == send) break;
      if (s+1 < send && *s == '\r' && *(s+1) == '\n')
        s++;
      if (*s != '\n') {
        if ((c1 = hex2int(*s)) == -1) break;
        if (++s == send) break;
        if ((c2 = hex2int(*s)) == -1) break;
        *ptr++ = (char)(c1 << 4 | c2);
      }
    }
    else {
      *ptr++ = *s;
    }
    s++;
    ss = s;
  }
  buf = mrb_str_resize(mrb, buf, (mrb_int)(ptr - RSTRING_PTR(buf)));
  mrb_str_cat(mrb, buf, ss, send-ss);
  mrb_ary_push(mrb, ary, buf);
  return slen;
}

static int
pack_nul(mrb_state *mrb, mrb_value dst, mrb_int didx, int count)
{
  long i;

  dst = str_len_ensure(mrb, dst, didx + count);
  for (i = 0; i < count; i++) {
    RSTRING_PTR(dst)[didx + i] = '\0';
  }
  return count;
}

static void
check_x(mrb_state *mrb, int a, int count, char c)
{
  if (a < count) {
    mrb_raisef(mrb, E_ARGUMENT_ERROR, "%c outside of string", c);
  }
}

static void
prepare_tmpl(mrb_state *mrb, struct tmpl *tmpl)
{
  mrb_get_args(mrb, "S", &tmpl->str);
  tmpl->idx = 0;
}

static int
has_tmpl(const struct tmpl *tmpl)
{
  return (tmpl->idx < RSTRING_LEN(tmpl->str));
}

static enum pack_dir
read_tmpl(mrb_state *mrb, struct tmpl *tmpl, enum pack_type *typep, int *sizep, int *countp, unsigned int *flagsp)
{
  mrb_int t, tlen;
  int ch, size = 0;
  enum pack_dir dir;
  enum pack_type type;
  int count = 1;
  unsigned int flags = 0;
  const char *tptr;

  tptr = RSTRING_PTR(tmpl->str);
  tlen = RSTRING_LEN(tmpl->str);

 restart:
  if (tmpl->idx >= tlen) return PACK_DIR_NONE;
  t = tptr[tmpl->idx++];
 alias:
  switch (t) {
  case 'A':
    dir = PACK_DIR_STR;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_WIDTH | PACK_FLAG_COUNT2;
    break;
  case 'a':
    dir = PACK_DIR_STR;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_WIDTH | PACK_FLAG_COUNT2 | PACK_FLAG_a;
    break;
  case 'C':
    dir = PACK_DIR_CHAR;
    type = PACK_TYPE_INTEGER;
    size = 1;
    break;
  case 'c':
    dir = PACK_DIR_CHAR;
    type = PACK_TYPE_INTEGER;
    size = 1;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'D': case 'd':
    dir = PACK_DIR_DOUBLE;
    type = PACK_TYPE_FLOAT;
    size = 8;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'F': case 'f':
    dir = PACK_DIR_FLOAT;
    type = PACK_TYPE_FLOAT;
    size = 4;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'E':
    dir = PACK_DIR_DOUBLE;
    type = PACK_TYPE_FLOAT;
    size = 8;
    flags |= PACK_FLAG_SIGNED | PACK_FLAG_LT;
    break;
  case 'e':
    dir = PACK_DIR_FLOAT;
    type = PACK_TYPE_FLOAT;
    size = 4;
    flags |= PACK_FLAG_SIGNED | PACK_FLAG_LT;
    break;
  case 'G':
    dir = PACK_DIR_DOUBLE;
    type = PACK_TYPE_FLOAT;
    size = 8;
    flags |= PACK_FLAG_SIGNED | PACK_FLAG_GT;
    break;
  case 'g':
    dir = PACK_DIR_FLOAT;
    type = PACK_TYPE_FLOAT;
    size = 4;
    flags |= PACK_FLAG_SIGNED | PACK_FLAG_GT;
    break;
  case 'H':
    dir = PACK_DIR_HEX;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_COUNT2;
    break;
  case 'h':
    dir = PACK_DIR_HEX;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_COUNT2 | PACK_FLAG_LSB;
    break;
  case 'B':
    dir = PACK_DIR_BSTR;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_COUNT2;
    break;
  case 'b':
    dir = PACK_DIR_BSTR;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_COUNT2 | PACK_FLAG_LSB;
    break;
  case 'I':
    switch (sizeof(int)) {
      case 2: t = 'S'; goto alias;
      case 4: t = 'L'; goto alias;
      case 8: t = 'Q'; goto alias;
      default:
        mrb_raisef(mrb, E_RUNTIME_ERROR, "mruby-pack does not support sizeof(int) == %d", (int)sizeof(int));
    }
    break;
  case 'i':
    switch (sizeof(int)) {
      case 2: t = 's'; goto alias;
      case 4: t = 'l'; goto alias;
      case 8: t = 'q'; goto alias;
      default:
        mrb_raisef(mrb, E_RUNTIME_ERROR, "mruby-pack does not support sizeof(int) == %d", (int)sizeof(int));
    }
    break;
  case 'J':
    switch (sizeof(intptr_t)) {
      case 4: t = 'L'; goto alias;
      case 8: t = 'Q'; goto alias;
      default:
        mrb_raisef(mrb, E_RUNTIME_ERROR, "mruby-pack does not support sizeof(uintptr_t) == %d", (int)sizeof(uintptr_t));
    }
    break;
  case 'j':
    switch (sizeof(intptr_t)) {
      case 4: t = 'l'; goto alias;
      case 8: t = 'q'; goto alias;
      default:
        mrb_raisef(mrb, E_RUNTIME_ERROR, "mruby-pack does not support sizeof(intptr_t) == %d", (int)sizeof(intptr_t));
    }
    break;
  case 'L':
    dir = PACK_DIR_LONG;
    type = PACK_TYPE_INTEGER;
    size = 4;
    break;
  case 'l':
    dir = PACK_DIR_LONG;
    type = PACK_TYPE_INTEGER;
    size = 4;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'w':
    dir = PACK_DIR_BER;
    type = PACK_TYPE_INTEGER;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'm':
    dir = PACK_DIR_BASE64;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_WIDTH | PACK_FLAG_COUNT2;
    break;
  case 'M':
    dir = PACK_DIR_QENC;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_WIDTH | PACK_FLAG_COUNT2;
    break;
  case 'N':  /* = "L>" */
    dir = PACK_DIR_LONG;
    type = PACK_TYPE_INTEGER;
    size = 4;
    flags |= PACK_FLAG_GT;
    break;
  case 'n':  /* = "S>" */
    dir = PACK_DIR_SHORT;
    type = PACK_TYPE_INTEGER;
    size = 2;
    flags |= PACK_FLAG_GT;
    break;
  case 'Q':
    dir = PACK_DIR_QUAD;
    type = PACK_TYPE_INTEGER;
    size = 8;
    break;
  case 'q':
    dir = PACK_DIR_QUAD;
    type = PACK_TYPE_INTEGER;
    size = 8;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'S':
    dir = PACK_DIR_SHORT;
    type = PACK_TYPE_INTEGER;
    size = 2;
    break;
  case 's':
    dir = PACK_DIR_SHORT;
    type = PACK_TYPE_INTEGER;
    size = 2;
    flags |= PACK_FLAG_SIGNED;
    break;
  case 'U':
    dir = PACK_DIR_UTF8;
    type = PACK_TYPE_INTEGER;
    break;
  case 'V':  /* = "L<" */
    dir = PACK_DIR_LONG;
    type = PACK_TYPE_INTEGER;
    size = 4;
    flags |= PACK_FLAG_LT;
    break;
  case 'v':  /* = "S<" */
    dir = PACK_DIR_SHORT;
    type = PACK_TYPE_INTEGER;
    size = 2;
    flags |= PACK_FLAG_LT;
    break;
  case 'x':
    dir = PACK_DIR_NUL;
    type = PACK_TYPE_NONE;
    break;
  case 'X':
    dir = PACK_DIR_BACK;
    type = PACK_TYPE_NONE;
    break;
  case '@':
    dir = PACK_DIR_ABS;
    type = PACK_TYPE_NONE;
    break;
  case 'Z':
    dir = PACK_DIR_STR;
    type = PACK_TYPE_STRING;
    flags |= PACK_FLAG_WIDTH | PACK_FLAG_COUNT2 | PACK_FLAG_Z;
    break;
  case '#':
    while (++tmpl->idx < tlen && tptr[tmpl->idx] != '\n')
      ;
    goto restart;

  case 'p': case 'P':
  case '%':
    mrb_raisef(mrb, E_ARGUMENT_ERROR, "%c is not supported", (char)t);
    break;
  default:
    if (!ISSPACE((char)t)) {
      char c = (char)t;
      mrb_value s = mrb_str_new(mrb, &c, 1);
      mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown unpack directive %!v", s);
    }
    goto restart;
  }

  /* read suffix [0-9*_!<>] */
  while (tmpl->idx < tlen) {
    ch = tptr[tmpl->idx];
    if (ISDIGIT(ch)) {
      char *e;
      mrb_int n;
      if (!mrb_read_int(tptr+tmpl->idx, tptr+tlen, &e, &n) || INT_MAX < n) {
        mrb_raise(mrb, E_RUNTIME_ERROR, "too big template length");
      }
      count = (int)n;
      tmpl->idx = (int)(e - tptr);
      continue;
    }
    else if (ch == '*')  {
      if (type == PACK_TYPE_NONE)
        count = 0;
      else
        count = -1;
    }
    else if (ch == '_' || ch == '!' || ch == '<' || ch == '>') {
      if (strchr("sSiIlLqQ", (int)t) == NULL) {
        mrb_raisef(mrb, E_ARGUMENT_ERROR, "'%c' allowed only after types sSiIlLqQ", ch);
      }
      if (ch == '_' || ch == '!') {
        flags |= PACK_FLAG_s;
      }
      else if (ch == '<') {
        flags |= PACK_FLAG_LT;
      }
      else if (ch == '>') {
        flags |= PACK_FLAG_GT;
      }
    }
    else {
      break;
    }
    tmpl->idx++;
  }

  if ((flags & PACK_FLAG_LT) || (!(flags & PACK_FLAG_GT) && littleendian)) {
    flags |= PACK_FLAG_LITTLEENDIAN;
  }

  *typep = type;
  *sizep = size;
  *countp = count;
  *flagsp = flags;
  return dir;
}

static mrb_value
mrb_pack_pack(mrb_state *mrb, mrb_value ary)
{
  mrb_value o, result;
  mrb_int aidx;
  struct tmpl tmpl;
  int count;
  unsigned int flags;
  enum pack_dir dir;
  enum pack_type type;
  int ridx, size;

  prepare_tmpl(mrb, &tmpl);

  result = mrb_str_new(mrb, NULL, 128);  /* allocate initial buffer */
  aidx = 0;
  ridx = 0;
  while (has_tmpl(&tmpl)) {
    dir = read_tmpl(mrb, &tmpl, &type, &size, &count, &flags);

    if (dir == PACK_DIR_NONE) break;
    if (dir == PACK_DIR_NUL) {
    grow:
      if (ridx > INT_MAX - count) goto overflow;
      ridx += pack_nul(mrb, result, ridx, count);
      continue;
    }
    else if (dir == PACK_DIR_BACK) {
      check_x(mrb, ridx, count, 'X');
      ridx -= count;
      continue;
    }
    else if (dir == PACK_DIR_ABS) {
      count -= ridx;
      if (count > 0) goto grow;
      count = -count;
      check_x(mrb, ridx, count, '@');
      ridx -= count;
      continue;
    }

    if ((flags & PACK_FLAG_WIDTH) && aidx >= RARRAY_LEN(ary)) {
      mrb_raise(mrb, E_ARGUMENT_ERROR, "too few arguments");
    }
    for (; aidx < RARRAY_LEN(ary); aidx++) {
      if (count == 0 && !(flags & PACK_FLAG_WIDTH))
        break;

      o = RARRAY_PTR(ary)[aidx];
      if (type == PACK_TYPE_INTEGER) {
        o = mrb_ensure_int_type(mrb, o);
      }
#ifndef MRB_NO_FLOAT
      else if (type == PACK_TYPE_FLOAT) {
        if (!mrb_float_p(o)) {
          o = mrb_ensure_float_type(mrb, o);
        }
      }
#endif
      else if (type == PACK_TYPE_STRING) {
        if (!mrb_string_p(o)) {
          mrb_raisef(mrb, E_TYPE_ERROR, "can't convert %T into String", o);
        }
      }

      switch (dir) {
      case PACK_DIR_CHAR:
        ridx += pack_char(mrb, o, result, ridx, flags);
        break;
      case PACK_DIR_SHORT:
        ridx += pack_short(mrb, o, result, ridx, flags);
        break;
      case PACK_DIR_LONG:
        ridx += pack_long(mrb, o, result, ridx, flags);
        break;
      case PACK_DIR_QUAD:
        ridx += pack_quad(mrb, o, result, ridx, flags);
        break;
      case PACK_DIR_BER:
        ridx += pack_BER(mrb, o, result, ridx, flags);
        break;
      case PACK_DIR_BASE64:
        ridx += pack_base64(mrb, o, result, ridx, count);
        break;
      case PACK_DIR_QENC:
        ridx += pack_qenc(mrb, o, result, ridx, count);
        break;
      case PACK_DIR_HEX:
        ridx += pack_hex(mrb, o, result, ridx, count, flags);
        break;
      case PACK_DIR_BSTR:
        ridx += pack_bstr(mrb, o, result, ridx, count, flags);
        break;
      case PACK_DIR_STR:
        ridx += pack_str(mrb, o, result, ridx, count, flags);
        break;
#ifndef MRB_NO_FLOAT
      case PACK_DIR_DOUBLE:
        ridx += pack_double(mrb, o, result, ridx, flags);
        break;
      case PACK_DIR_FLOAT:
        ridx += pack_float(mrb, o, result, ridx, flags);
        break;
#endif
      case PACK_DIR_UTF8:
        ridx += pack_utf8(mrb, o, result, ridx, count, flags);
        break;
      default:
        break;
      }
      if (flags & PACK_FLAG_COUNT2) {
        /* always consumes 1 entry */
        aidx++;
        break;
      }
      if (count > 0) {
        count--;
      }
    }
    if (ridx < 0) {
    overflow:
      mrb_raise(mrb, E_RANGE_ERROR, "negative (or overflowed) template size");
    }
  }

  mrb_str_resize(mrb, result, ridx);
  return result;
}

static mrb_value
pack_unpack(mrb_state *mrb, mrb_value str, int single)
{
  mrb_value result;
  struct tmpl tmpl;
  int count;
  unsigned int flags;
  enum pack_dir dir;
  enum pack_type type;
  int size;
  int srcidx, srclen;
  const unsigned char *sptr;

  prepare_tmpl(mrb, &tmpl);

  srcidx = 0;
  srclen = (int)RSTRING_LEN(str);

  result = mrb_ary_new(mrb);
  while (has_tmpl(&tmpl)) {
    dir = read_tmpl(mrb, &tmpl, &type, &size, &count, &flags);

    if (dir == PACK_DIR_NONE) break;
    if (dir == PACK_DIR_NUL) {
      check_x(mrb, srclen-srcidx, count, 'x');
      srcidx += count;
      continue;
    }
    else if (dir == PACK_DIR_BACK) {
      check_x(mrb, srcidx, count, 'X');
      srcidx -= count;
      continue;
    }
    else if (dir == PACK_DIR_ABS) {
      check_x(mrb, srclen, count, '@');
      srcidx = count;
      continue;
    }

    /* PACK_FLAG_COUNT2 directions */
    sptr = (const unsigned char*)RSTRING_PTR(str) + srcidx;
    switch (dir) {
    case PACK_DIR_HEX:
      srcidx += unpack_hex(mrb, sptr, srclen - srcidx, result, count, flags);
      continue;
    case PACK_DIR_BSTR:
      srcidx += unpack_bstr(mrb, sptr, srclen - srcidx, result, count, flags);
      continue;
    case PACK_DIR_STR:
      srcidx += unpack_str(mrb, sptr, srclen - srcidx, result, count, flags);
      continue;
    case PACK_DIR_BASE64:
      srcidx += unpack_base64(mrb, sptr, srclen - srcidx, result);
      continue;
    case PACK_DIR_QENC:
      srcidx += unpack_qenc(mrb, sptr, srclen - srcidx, result);
      continue;
    default:
      break;
    }

    while (count != 0 && srcidx < srclen) {
      if (srclen - srcidx < size) {
        while (count-- > 0) {
          mrb_ary_push(mrb, result, mrb_nil_value());
        }
        break;
      }

      sptr = (const unsigned char*)RSTRING_PTR(str) + srcidx;
      switch (dir) {
      case PACK_DIR_CHAR:
        srcidx += unpack_char(mrb, sptr, srclen - srcidx, result, flags);
        break;
      case PACK_DIR_SHORT:
        srcidx += unpack_short(mrb, sptr, srclen - srcidx, result, flags);
        break;
      case PACK_DIR_LONG:
        srcidx += unpack_long(mrb, sptr, srclen - srcidx, result, flags);
        break;
      case PACK_DIR_QUAD:
        srcidx += unpack_quad(mrb, sptr, srclen - srcidx, result, flags);
        break;
      case PACK_DIR_BER:
        srcidx += unpack_BER(mrb, sptr, srclen - srcidx, result, flags);
        break;
#ifndef MRB_NO_FLOAT
      case PACK_DIR_FLOAT:
        srcidx += unpack_float(mrb, sptr, srclen - srcidx, result, flags);
        break;
      case PACK_DIR_DOUBLE:
        srcidx += unpack_double(mrb, sptr, srclen - srcidx, result, flags);
        break;
#endif
      case PACK_DIR_UTF8:
        srcidx += unpack_utf8(mrb, sptr, srclen - srcidx, result, flags);
        break;
      default:
        mrb_raise(mrb, E_RUNTIME_ERROR, "mruby-pack's bug");
      }
      if (count > 0) {
        count--;
      }
    }
    if (single) {
      if (RARRAY_LEN(result) > 0) {
        return RARRAY_PTR(result)[0];
      }
      return mrb_nil_value();
    }
  }
  return result;
}

static mrb_value
mrb_pack_unpack(mrb_state *mrb, mrb_value str)
{
  return pack_unpack(mrb, str, 0);
}

static mrb_value
mrb_pack_unpack1(mrb_state *mrb, mrb_value str)
{
  return pack_unpack(mrb, str, 1);
}

void
mrb_mruby_pack_gem_init(mrb_state *mrb)
{
  mrb_define_method(mrb, mrb->array_class, "pack", mrb_pack_pack, MRB_ARGS_REQ(1));
  mrb_define_method(mrb, mrb->string_class, "unpack", mrb_pack_unpack, MRB_ARGS_REQ(1));
  mrb_define_method(mrb, mrb->string_class, "unpack1", mrb_pack_unpack1, MRB_ARGS_REQ(1));
}

void
mrb_mruby_pack_gem_final(mrb_state *mrb)
{
}
