/** @file

  A brief file description

  @section license License

  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */

#include "ts/ink_platform.h"
#include "P_Cache.h"
#include "ts/I_Layout.h"
#include "ts/ink_file.h"
#include "ts/Tokenizer.h"
#include "ts/SimpleTokenizer.h"

#if HAVE_LINUX_MAJOR_H
#include <linux/major.h>
#endif

//
// Store
//

const char Store::VOLUME_KEY[]           = "volume";
const char Store::HASH_BASE_STRING_KEY[] = "id";

static span_error_t
make_span_error(int error)
{
  switch (error) {
  case ENOENT:
    return SPAN_ERROR_NOT_FOUND;
  case EPERM: /* fallthru */
  case EACCES:
    return SPAN_ERROR_NO_ACCESS;
  default:
    return SPAN_ERROR_UNKNOWN;
  }
}

static const char *
span_file_typename(mode_t st_mode)
{
  switch (st_mode & S_IFMT) {
  case S_IFBLK:
    return "block device";
  case S_IFCHR:
    return "character device";
  case S_IFDIR:
    return "directory";
  case S_IFREG:
    return "file";
  default:
    return "<unsupported>";
  }
}

Ptr<ProxyMutex> tmp_p;
Store::Store() : n_disks_in_config(0), n_disks(0), disk(nullptr)
{
}

void
Store::add(Span *ds)
{
  extend(n_disks + 1);
  disk[n_disks - 1] = ds;
}

void
Store::add(Store &s)
{
  // assume on different disks
  for (unsigned i = 0; i < s.n_disks; i++) {
    add(s.disk[i]);
  }
  s.n_disks = 0;
  s.delete_all();
}

// should be changed to handle offset in more general
// case (where this is not a free of a "just" allocated
// store
void
Store::free(Store &s)
{
  for (unsigned i = 0; i < s.n_disks; i++) {
    for (Span *sd = s.disk[i]; sd; sd = sd->link.next) {
      for (unsigned j = 0; j < n_disks; j++)
        for (Span *d = disk[j]; d; d = d->link.next)
          if (!strcmp(sd->pathname, d->pathname)) {
            if (sd->offset < d->offset)
              d->offset = sd->offset;
            d->blocks += sd->blocks;
            goto Lfound;
          }
      ink_release_assert(!"Store::free failed");
    Lfound:;
    }
  }
}

void
Store::sort()
{
  Span **vec = (Span **)alloca(sizeof(Span *) * n_disks);
  memset(vec, 0, sizeof(Span *) * n_disks);
  for (unsigned i = 0; i < n_disks; i++) {
    vec[i]  = disk[i];
    disk[i] = nullptr;
  }

  // sort by device

  unsigned n = 0;
  for (unsigned i = 0; i < n_disks; i++) {
    for (Span *sd = vec[i]; sd; sd = vec[i]) {
      vec[i] = vec[i]->link.next;
      for (unsigned d = 0; d < n; d++) {
        if (sd->disk_id == disk[d]->disk_id) {
          sd->link.next = disk[d];
          disk[d]       = sd;
          goto Ldone;
        }
      }
      disk[n++] = sd;
    Ldone:;
    }
  }
  n_disks = n;

  // sort by pathname x offset

  for (unsigned i = 0; i < n_disks; i++) {
  Lagain:
    Span *prev = nullptr;
    for (Span *sd = disk[i]; sd;) {
      Span *next = sd->link.next;
      if (next &&
          ((strcmp(sd->pathname, next->pathname) < 0) || (!strcmp(sd->pathname, next->pathname) && sd->offset > next->offset))) {
        if (!prev) {
          disk[i]         = next;
          sd->link.next   = next->link.next;
          next->link.next = sd;
        } else {
          prev->link.next = next;
          sd->link.next   = next->link.next;
          next->link.next = sd;
        }
        goto Lagain;
      }
      prev = sd;
      sd   = next;
    }
  }

  // merge adjacent spans

  for (unsigned i = 0; i < n_disks; i++) {
    for (Span *sd = disk[i]; sd;) {
      Span *next = sd->link.next;
      if (next && !strcmp(sd->pathname, next->pathname)) {
        if (!sd->file_pathname) {
          sd->blocks += next->blocks;
        } else if (next->offset <= sd->end()) {
          if (next->end() >= sd->end())
            sd->blocks += (next->end() - sd->end());
        } else {
          sd = next;
          continue;
        }
        sd->link.next = next->link.next;
        delete next;
        sd = sd->link.next;
      } else
        sd = next;
    }
  }
}

const char *
Span::errorstr(span_error_t serr)
{
  switch (serr) {
  case SPAN_ERROR_OK:
    return "no error";
  case SPAN_ERROR_NOT_FOUND:
    return "file not found";
  case SPAN_ERROR_NO_ACCESS:
    return "unable to access file";
  case SPAN_ERROR_MISSING_SIZE:
    return "missing size specification";
  case SPAN_ERROR_UNSUPPORTED_DEVTYPE:
    return "unsupported cache file type";
  case SPAN_ERROR_MEDIA_PROBE:
    return "failed to probe device geometry";
  case SPAN_ERROR_UNKNOWN: /* fallthru */
  default:
    return "unknown error";
  }
}

int
Span::path(char *filename, int64_t *aoffset, char *buf, int buflen)
{
  ink_assert(!aoffset);
  Span *ds = this;

  if ((strlen(ds->pathname) + strlen(filename) + 2) > (size_t)buflen)
    return -1;
  if (!ds->file_pathname) {
    ink_filepath_make(buf, buflen, ds->pathname, filename);
  } else {
    ink_strlcpy(buf, ds->pathname, buflen);
  }

  return strlen(buf);
}

void
Span::hash_base_string_set(const char *s)
{
  hash_base_string = s ? ats_strdup(s) : nullptr;
}

void
Span::volume_number_set(int n)
{
  forced_volume_num = n;
}

void
Store::delete_all()
{
  for (unsigned i = 0; i < n_disks; i++) {
    if (disk[i])
      delete disk[i];
  }
  n_disks = 0;
  ats_free(disk);
  disk = nullptr;
}

Store::~Store()
{
  delete_all();
}

Span::~Span()
{
  if (link.next)
    delete link.next;
}

static int
get_int64(int fd, int64_t &data)
{
  char buf[PATH_NAME_MAX];
  if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
    return (-1);
  // the above line will guarantee buf to be no longer than PATH_NAME_MAX
  // so the next statement should be a safe use of sscanf
  // coverity[secure_coding]
  if (sscanf(buf, "%" PRId64 "", &data) != 1) {
    return (-1);
  }
  return 0;
}

int
Store::remove(char *n)
{
  bool found = false;
Lagain:
  for (unsigned i = 0; i < n_disks; i++) {
    Span *p = nullptr;
    for (Span *sd = disk[i]; sd; sd = sd->link.next) {
      if (!strcmp(n, sd->pathname)) {
        found = true;
        if (p)
          p->link.next = sd->link.next;
        else
          disk[i]     = sd->link.next;
        sd->link.next = nullptr;
        delete sd;
        goto Lagain;
      }
      p = sd;
    }
  }
  return found ? 0 : -1;
}

const char *
Store::read_config()
{
  int n_dsstore   = 0;
  int ln          = 0;
  int i           = 0;
  const char *err = nullptr;
  Span *sd = nullptr, *cur = nullptr;
  Span *ns;
  ats_scoped_fd fd;
  ats_scoped_str storage_path(RecConfigReadConfigPath("proxy.config.cache.storage_filename", "storage.config"));

  Debug("cache_init", "Store::read_config, fd = -1, \"%s\"", (const char *)storage_path);
  fd = ::open(storage_path, O_RDONLY);
  if (fd < 0) {
    err = "error on open";
    goto Lfail;
  }

  // For each line

  char line[1024];
  int len;
  while ((len = ink_file_fd_readline(fd, sizeof(line), line)) > 0) {
    const char *path;
    const char *seed = nullptr;
    // update lines

    ++ln;

    // Because the SimpleTokenizer is a bit too simple, we have to normalize whitespace.
    for (char *spot = line, *limit = line + len; spot < limit; ++spot)
      if (ParseRules::is_space(*spot))
        *spot = ' '; // force whitespace to literal space.

    SimpleTokenizer tokens(line, ' ', SimpleTokenizer::OVERWRITE_INPUT_STRING);

    // skip comments and blank lines
    path = tokens.getNext();
    if (nullptr == path || '#' == path[0])
      continue;

    // parse
    Debug("cache_init", "Store::read_config: \"%s\"", path);
    ++n_disks_in_config;

    int64_t size   = -1;
    int volume_num = -1;
    const char *e;
    while (nullptr != (e = tokens.getNext())) {
      if (ParseRules::is_digit(*e)) {
        if ((size = ink_atoi64(e)) <= 0) {
          err = "error parsing size";
          goto Lfail;
        }
      } else if (0 == strncasecmp(HASH_BASE_STRING_KEY, e, sizeof(HASH_BASE_STRING_KEY) - 1)) {
        e += sizeof(HASH_BASE_STRING_KEY) - 1;
        if ('=' == *e)
          ++e;
        if (*e && !ParseRules::is_space(*e))
          seed = e;
      } else if (0 == strncasecmp(VOLUME_KEY, e, sizeof(VOLUME_KEY) - 1)) {
        e += sizeof(VOLUME_KEY) - 1;
        if ('=' == *e)
          ++e;
        if (!*e || !ParseRules::is_digit(*e) || 0 >= (volume_num = ink_atoi(e))) {
          err = "error parsing volume number";
          goto Lfail;
        }
      }
    }

    std::string pp = Layout::get()->relative(path);

    ns = new Span;
    Debug("cache_init", "Store::read_config - ns = new Span; ns->init(\"%s\",%" PRId64 "), forced volume=%d%s%s", pp.c_str(), size,
          volume_num, seed ? " id=" : "", seed ? seed : "");
    if ((err = ns->init(pp.c_str(), size))) {
      RecSignalWarning(REC_SIGNAL_SYSTEM_ERROR, "could not initialize storage \"%s\" [%s]", pp.c_str(), err);
      Debug("cache_init", "Store::read_config - could not initialize storage \"%s\" [%s]", pp.c_str(), err);
      delete ns;
      continue;
    }

    n_dsstore++;

    // Set side values if present.
    if (seed)
      ns->hash_base_string_set(seed);
    if (volume_num > 0)
      ns->volume_number_set(volume_num);

    // new Span
    {
      Span *prev = cur;
      cur        = ns;
      if (!sd)
        sd = cur;
      else
        prev->link.next = cur;
    }
  }

  // count the number of disks
  extend(n_dsstore);
  cur = sd;
  while (cur) {
    Span *next     = cur->link.next;
    cur->link.next = nullptr;
    disk[i++]      = cur;
    cur            = next;
  }
  sd = nullptr; // these are all used.
  sort();
  return nullptr;

Lfail:
  // Do clean up.
  if (sd)
    delete sd;

  return err;
}

int
Store::write_config_data(int fd) const
{
  for (unsigned i = 0; i < n_disks; i++)
    for (Span *sd = disk[i]; sd; sd = sd->link.next) {
      char buf[PATH_NAME_MAX + 64];
      snprintf(buf, sizeof(buf), "%s %" PRId64 "\n", sd->pathname.get(), (int64_t)sd->blocks * (int64_t)STORE_BLOCK_SIZE);
      if (ink_file_fd_writestring(fd, buf) == -1)
        return (-1);
    }
  return 0;
}

const char *
Span::init(const char *path, int64_t size)
{
  struct stat sbuf;
  struct statvfs vbuf;
  span_error_t serr;
  ink_device_geometry geometry;

  ats_scoped_fd fd(socketManager.open(path, O_RDONLY));
  if (fd < 0) {
    serr = make_span_error(errno);
    Warning("unable to open '%s': %s", path, strerror(errno));
    goto fail;
  }

  if (fstat(fd, &sbuf) == -1) {
    serr = make_span_error(errno);
    Warning("unable to stat '%s': %s (%d)", path, strerror(errno), errno);
    goto fail;
  }

  if (fstatvfs(fd, &vbuf) == -1) {
    serr = make_span_error(errno);
    Warning("unable to statvfs '%s': %s (%d)", path, strerror(errno), errno);
    goto fail;
  }

  // Directories require an explicit size parameter. For device nodes and files, we use
  // the existing size.
  if (S_ISDIR(sbuf.st_mode)) {
    if (size <= 0) {
      Warning("cache %s '%s' requires a size > 0", span_file_typename(sbuf.st_mode), path);
      serr = SPAN_ERROR_MISSING_SIZE;
      goto fail;
    }
  }

  // Should regular files use the IO size (vbuf.f_bsize), or the
  // fundamental filesystem block size (vbuf.f_frsize)? That depends
  // on whether we are using that block size for performance or for
  // reliability.

  switch (sbuf.st_mode & S_IFMT) {
  case S_IFBLK:
  case S_IFCHR:

#if defined(linux)
    if (major(sbuf.st_rdev) == RAW_MAJOR && minor(sbuf.st_rdev) == 0) {
      Warning("cache %s '%s' is the raw device control interface", span_file_typename(sbuf.st_mode), path);
      serr = SPAN_ERROR_UNSUPPORTED_DEVTYPE;
      goto fail;
    }
#endif

    if (!ink_file_get_geometry(fd, geometry)) {
      serr = make_span_error(errno);

      if (errno == ENOTSUP) {
        Warning("failed to query disk geometry for '%s', no raw device support", path);
      } else {
        Warning("failed to query disk geometry for '%s', %s (%d)", path, strerror(errno), errno);
      }

      goto fail;
    }

    this->disk_id[0]     = 0;
    this->disk_id[1]     = sbuf.st_rdev;
    this->file_pathname  = true;
    this->hw_sector_size = geometry.blocksz;
    this->alignment      = geometry.alignsz;
    this->blocks         = geometry.totalsz / STORE_BLOCK_SIZE;

    break;

  case S_IFDIR:
    if ((int64_t)(vbuf.f_frsize * vbuf.f_bavail) < size) {
      Warning("not enough free space for cache %s '%s'", span_file_typename(sbuf.st_mode), path);
      // Just warn for now; let the cache open fail later.
    }

    // The cache initialization code in Cache.cc takes care of creating the actual cache file, naming it and making
    // it the right size based on the "file_pathname" flag. That's something that we should clean up in the future.
    this->file_pathname = false;

    this->disk_id[0]     = sbuf.st_dev;
    this->disk_id[1]     = sbuf.st_ino;
    this->hw_sector_size = vbuf.f_bsize;
    this->alignment      = 0;
    this->blocks         = size / STORE_BLOCK_SIZE;
    break;

  case S_IFREG:
    if (size > 0 && sbuf.st_size < size) {
      int64_t needed = size - sbuf.st_size;
      if ((int64_t)(vbuf.f_frsize * vbuf.f_bavail) < needed) {
        Warning("not enough free space for cache %s '%s'", span_file_typename(sbuf.st_mode), path);
        // Just warn for now; let the cache open fail later.
      }
    }

    this->disk_id[0]     = sbuf.st_dev;
    this->disk_id[1]     = sbuf.st_ino;
    this->file_pathname  = true;
    this->hw_sector_size = vbuf.f_bsize;
    this->alignment      = 0;
    this->blocks         = sbuf.st_size / STORE_BLOCK_SIZE;

    break;

  default:
    serr = SPAN_ERROR_UNSUPPORTED_DEVTYPE;
    goto fail;
  }

  // The actual size of a span always trumps the configured size.
  if (size > 0 && this->size() != size) {
    int64_t newsz = std::min(size, this->size());

    Note("cache %s '%s' is %" PRId64 " bytes, but the configured size is %" PRId64 " bytes, using the minimum",
         span_file_typename(sbuf.st_mode), path, this->size(), size);

    this->blocks = newsz / STORE_BLOCK_SIZE;
  }

  // A directory span means we will end up with a file, otherwise, we get what we asked for.
  this->set_mmapable(ink_file_is_mmappable(S_ISDIR(sbuf.st_mode) ? (mode_t)S_IFREG : sbuf.st_mode));
  this->pathname = ats_strdup(path);

  Debug("cache_init", "initialized span '%s'", this->pathname.get());
  Debug("cache_init", "hw_sector_size=%d, size=%" PRId64 ", blocks=%" PRId64 ", disk_id=%" PRId64 "/%" PRId64 ", file_pathname=%d",
        this->hw_sector_size, this->size(), this->blocks, this->disk_id[0], this->disk_id[1], this->file_pathname);

  return nullptr;

fail:
  return Span::errorstr(serr);
}

void
Store::normalize()
{
  unsigned ndisks = 0;
  for (unsigned i = 0; i < n_disks; i++) {
    if (disk[i])
      disk[ndisks++] = disk[i];
  }
  n_disks = ndisks;
}

static unsigned int
try_alloc(Store &target, Span *source, unsigned int start_blocks, bool one_only = false)
{
  unsigned int blocks = start_blocks;
  Span *ds            = nullptr;
  while (source && blocks) {
    if (source->blocks) {
      unsigned int a; // allocated
      if (blocks > source->blocks)
        a = source->blocks;
      else
        a     = blocks;
      Span *d = new Span(*source);

      d->blocks    = a;
      d->link.next = ds;

      if (d->file_pathname)
        source->offset += a;
      source->blocks -= a;
      ds = d;
      blocks -= a;
      if (one_only)
        break;
    }
    source = source->link.next;
  }
  if (ds)
    target.add(ds);
  return start_blocks - blocks;
}

void
Store::spread_alloc(Store &s, unsigned int blocks, bool mmapable)
{
  //
  // Count the eligable disks..
  //
  int mmapable_disks = 0;
  for (unsigned k = 0; k < n_disks; k++) {
    if (disk[k]->is_mmapable()) {
      mmapable_disks++;
    }
  }

  int spread_over = n_disks;
  if (mmapable) {
    spread_over = mmapable_disks;
  }

  if (spread_over == 0) {
    return;
  }

  int disks_left = spread_over;

  for (unsigned i = 0; blocks && disks_left && i < n_disks; i++) {
    if (!(mmapable && !disk[i]->is_mmapable())) {
      unsigned int target = blocks / disks_left;
      if (blocks - target > total_blocks(i + 1))
        target = blocks - total_blocks(i + 1);
      blocks -= try_alloc(s, disk[i], target);
      disks_left--;
    }
  }
}

void
Store::try_realloc(Store &s, Store &diff)
{
  for (unsigned i = 0; i < s.n_disks; i++) {
    Span *prev = nullptr;
    for (Span *sd = s.disk[i]; sd;) {
      for (unsigned j = 0; j < n_disks; j++)
        for (Span *d = disk[j]; d; d = d->link.next)
          if (!strcmp(sd->pathname, d->pathname)) {
            if (sd->offset >= d->offset && (sd->end() <= d->end())) {
              if (!sd->file_pathname || (sd->end() == d->end())) {
                d->blocks -= sd->blocks;
                goto Lfound;
              } else if (sd->offset == d->offset) {
                d->blocks -= sd->blocks;
                d->offset += sd->blocks;
                goto Lfound;
              } else {
                Span *x = new Span(*d);
                // d will be the first vol
                d->blocks    = sd->offset - d->offset;
                d->link.next = x;
                // x will be the last vol
                x->offset = sd->offset + sd->blocks;
                x->blocks -= x->offset - d->offset;
                goto Lfound;
              }
            }
          }
      {
        if (!prev)
          s.disk[i] = s.disk[i]->link.next;
        else
          prev->link.next = sd->link.next;
        diff.extend(i + 1);
        sd->link.next = diff.disk[i];
        diff.disk[i]  = sd;
        sd            = prev ? prev->link.next : s.disk[i];
        continue;
      }
    Lfound:;
      prev = sd;
      sd   = sd->link.next;
    }
  }
  normalize();
  s.normalize();
  diff.normalize();
}

//
// Stupid grab first availabled space allocator
//
void
Store::alloc(Store &s, unsigned int blocks, bool one_only, bool mmapable)
{
  unsigned int oblocks = blocks;
  for (unsigned i = 0; blocks && i < n_disks; i++) {
    if (!(mmapable && !disk[i]->is_mmapable())) {
      blocks -= try_alloc(s, disk[i], blocks, one_only);
      if (one_only && oblocks != blocks)
        break;
    }
  }
}

int
Span::write(int fd) const
{
  char buf[32];

  if (ink_file_fd_writestring(fd, (pathname ? pathname.get() : ")")) == -1)
    return (-1);
  if (ink_file_fd_writestring(fd, "\n") == -1)
    return (-1);

  snprintf(buf, sizeof(buf), "%" PRId64 "\n", blocks);
  if (ink_file_fd_writestring(fd, buf) == -1)
    return (-1);

  snprintf(buf, sizeof(buf), "%d\n", file_pathname);
  if (ink_file_fd_writestring(fd, buf) == -1)
    return (-1);

  snprintf(buf, sizeof(buf), "%" PRId64 "\n", offset);
  if (ink_file_fd_writestring(fd, buf) == -1)
    return (-1);

  snprintf(buf, sizeof(buf), "%d\n", (int)is_mmapable());
  if (ink_file_fd_writestring(fd, buf) == -1)
    return (-1);

  return 0;
}

int
Store::write(int fd, const char *name) const
{
  char buf[32];

  if (ink_file_fd_writestring(fd, name) == -1)
    return (-1);
  if (ink_file_fd_writestring(fd, "\n") == -1)
    return (-1);

  snprintf(buf, sizeof(buf), "%d\n", n_disks);
  if (ink_file_fd_writestring(fd, buf) == -1)
    return (-1);

  for (unsigned i = 0; i < n_disks; i++) {
    int n    = 0;
    Span *sd = nullptr;
    for (sd = disk[i]; sd; sd = sd->link.next) {
      n++;
    }

    snprintf(buf, sizeof(buf), "%d\n", n);
    if (ink_file_fd_writestring(fd, buf) == -1)
      return (-1);

    for (sd = disk[i]; sd; sd = sd->link.next) {
      if (sd->write(fd))
        return -1;
    }
  }
  return 0;
}

int
Span::read(int fd)
{
  char buf[PATH_NAME_MAX], p[PATH_NAME_MAX];

  if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
    return (-1);
  // the above line will guarantee buf to be no longer than PATH_NAME_MAX
  // so the next statement should be a safe use of sscanf
  // coverity[secure_coding]
  if (sscanf(buf, "%s", p) != 1) {
    return (-1);
  }
  pathname = ats_strdup(p);
  if (get_int64(fd, blocks) < 0) {
    return -1;
  }

  int b = 0;
  if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
    return (-1);
  // the above line will guarantee buf to be no longer than PATH_NAME_MAX
  // so the next statement should be a safe use of sscanf
  // coverity[secure_coding]
  if (sscanf(buf, "%d", &b) != 1)
    return (-1);
  file_pathname = (b ? true : false);

  if (get_int64(fd, offset) < 0) {
    return -1;
  }

  int tmp;
  if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
    return (-1);
  // the above line will guarantee buf to be no longer than PATH_NAME_MAX
  // so the next statement should be a safe use of sscanf
  // coverity[secure_coding]
  if (sscanf(buf, "%d", &tmp) != 1)
    return (-1);
  set_mmapable(tmp != 0);

  return (0);
}

int
Store::read(int fd, char *aname)
{
  char *name = aname;
  char tname[PATH_NAME_MAX];
  char buf[PATH_NAME_MAX];
  if (!aname)
    name = tname;

  if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
    return (-1);
  // the above line will guarantee buf to be no longer than PATH_NAME_MAX
  // so the next statement should be a safe use of sscanf
  // coverity[secure_coding]
  if (sscanf(buf, "%s\n", name) != 1)
    return (-1);

  if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
    return (-1);
  // the above line will guarantee buf to be no longer than PATH_NAME_MAX
  // so the next statement should be a safe use of sscanf
  // coverity[secure_coding]
  if (sscanf(buf, "%d\n", &n_disks) != 1)
    return (-1);

  disk = (Span **)ats_malloc(sizeof(Span *) * n_disks);
  if (!disk)
    return -1;
  memset(disk, 0, sizeof(Span *) * n_disks);
  for (unsigned i = 0; i < n_disks; i++) {
    int n = 0;

    if (ink_file_fd_readline(fd, PATH_NAME_MAX, buf) <= 0)
      return (-1);
    // the above line will guarantee buf to be no longer than PATH_NAME_MAX
    // so the next statement should be a safe use of sscanf
    // coverity[secure_coding]
    if (sscanf(buf, "%d\n", &n) != 1)
      return (-1);

    Span *sd = nullptr;
    while (n--) {
      Span *last = sd;
      sd         = new Span;

      if (!last)
        disk[i] = sd;
      else
        last->link.next = sd;
      if (sd->read(fd))
        goto Lbail;
    }
  }
  return 0;
Lbail:
  for (unsigned i = 0; i < n_disks; i++) {
    if (disk[i])
      delete disk[i];
  }
  return -1;
}

Span *
Span::dup()
{
  Span *ds = new Span(*this);
  if (this->link.next)
    ds->link.next = this->link.next->dup();
  return ds;
}

void
Store::dup(Store &s)
{
  s.n_disks = n_disks;
  s.disk    = (Span **)ats_malloc(sizeof(Span *) * n_disks);
  for (unsigned i = 0; i < n_disks; i++) {
    s.disk[i] = disk[i]->dup();
  }
}

int
Store::clear(char *filename, bool clear_dirs)
{
  char z[STORE_BLOCK_SIZE];
  memset(z, 0, STORE_BLOCK_SIZE);
  for (unsigned i = 0; i < n_disks; i++) {
    Span *ds = disk[i];
    for (unsigned j = 0; j < disk[i]->paths(); j++) {
      char path[PATH_NAME_MAX];
      Span *d = ds->nth(j);
      if (!clear_dirs && !d->file_pathname)
        continue;
      int r = d->path(filename, nullptr, path, PATH_NAME_MAX);
      if (r < 0)
        return -1;
      int fd = ::open(path, O_RDWR | O_CREAT, 0644);
      if (fd < 0)
        return -1;
      for (int b = 0; d->blocks; b++)
        if (socketManager.pwrite(fd, z, STORE_BLOCK_SIZE, d->offset + (b * STORE_BLOCK_SIZE)) < 0) {
          close(fd);
          return -1;
        }
      close(fd);
    }
  }
  return 0;
}
