///|
priv struct Member {
  id : Bytes
  name : String
  length : UInt64
  hash : Bytes
  desc : Bytes
  sums : Bytes
  recoverable : Bool
}

///|
pub fn safe_name(name : String) -> Bool {
  if name.is_empty() ||
    name.length() > 255 ||
    name == "." ||
    name == ".." ||
    name.has_suffix(".") ||
    name.has_suffix(" ") {
    return false
  }
  for c in name {
    if c < ' ' ||
      c == '\u007f' ||
      ['/', '\\', ':', '<', '>', '"', '|', '?', '*'].contains(c) {
      return false
    }
  }
  let mut base = name.to_lower().split(".").next().unwrap().to_owned()
  while base.has_suffix(" ") {
    base = base[:base.length() - 1].to_owned()
  }
  if ["con", "prn", "aux", "nul"].contains(base) {
    return false
  }
  for i = 0; i <= 9; i = i + 1 {
    if base == "com\{i}" || base == "lpt\{i}" {
      return false
    }
  }
  true
}

///|
/// Portable, root-relative PAR2 path. Never accepts an absolute or parent path.
pub fn safe_path(path : String) -> Bool {
  if path.is_empty() || path.length() > 4096 {
    return false
  }
  let parts = path.split("/").to_array()
  if parts.length() > 64 {
    return false
  }
  for part in parts {
    if !safe_name(part.to_owned()) {
      return false
    }
  }
  true
}

///|
fn claim_path(names : Map[String, Bool], path : String) -> Unit raise {
  if !safe_path(path) {
    fail("unsafe or unsupported relative path: " + path)
  }
  let key = path.to_lower()
  if names.contains(key) {
    fail("duplicate or case-colliding path: " + path)
  }
  for old, _ in names {
    if key.has_prefix(old + "/") || old.has_prefix(key + "/") {
      fail("file/directory path collision: " + path)
    }
  }
  names[key] = true
}

///|
fn slice_at(data : Bytes, start : Int32, width : Int32) -> BytesView {
  if data.length() - start >= width {
    return data[start:start + width]
  }
  let padded = Bytes::makei(width, fn(j) {
    if start + j < data.length() {
      data[start + j]
    } else {
      b'\x00'
    }
  })
  padded[:]
}

///|
fn xor_slice(
  row : Array[Int32],
  offset : Int32,
  data : BytesView,
  factor : Int32,
) -> Unit {
  if factor == 0 {
    return
  }
  let log = field_logs[factor]
  for j = 0; j < data.length() / 2; j = j + 1 {
    let word = data[j * 2].to_int() | (data[j * 2 + 1].to_int() << 8)
    if word != 0 {
      let e = log + field_logs[word]
      row[offset + j] = row[offset + j] ^
        field_powers[if e >= 65535 { e - 65535 } else { e }]
    }
  }
}

///|
fn as_words(block : Bytes) -> Array[Int32] {
  Array::makei(block.length() / 2, fn(i) {
    block[i * 2].to_int() | (block[i * 2 + 1].to_int() << 8)
  })
}

///|
fn as_bytes(words : Array[Int32]) -> Bytes {
  Bytes::makei(words.length() * 2, fn(i) {
    (words[i / 2] >> (8 * (i % 2))).to_byte()
  })
}

///|
test "portable names and partial slice padding" {
  assert_true(safe_name("photo 1.jpg"))
  for bad in ["../x", "NUL.txt", ".", "AUX", "COM1.cfg", "a/b", "a:"] {
    assert_false(safe_name(bad))
  }
  assert_eq(slice_at(b"12345", 4, 4).to_owned(), b"5\x00\x00\x00")
}