/*
 * utility.cpp
 *
 * Home page of code is: https://www.smartmontools.org
 *
 * Copyright (C) 2002-12 Bruce Allen
 * Copyright (C) 2008-23 Christian Franke
 * Copyright (C) 2000 Michael Cornwell <cornwell@acm.org>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

// THIS FILE IS INTENDED FOR UTILITY ROUTINES THAT ARE APPLICABLE TO
// BOTH SCSI AND ATA DEVICES, AND THAT MAY BE USED IN SMARTD,
// SMARTCTL, OR BOTH.

#include "config.h"
#define __STDC_FORMAT_MACROS 1 // enable PRI* for C++

#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdarg.h>
#include <sys/stat.h>
#ifdef HAVE_LOCALE_H
#include <locale.h>
#endif
#ifdef _WIN32
#include <mbstring.h> // _mbsinc()
#endif

#include <stdexcept>

#include "svnversion.h"
#include "utility.h"

#include "atacmds.h"
#include "dev_interface.h"
#include "sg_unaligned.h"

#ifndef USE_CLOCK_MONOTONIC
#ifdef __MINGW32__
// If MinGW-w64 < 9.0.0 or Windows < 8, GetSystemTimeAsFileTime() is used for
// std::chrono::high_resolution_clock.  This provides only 1/64s (>15ms) resolution.
// CLOCK_MONOTONIC uses QueryPerformanceCounter() which provides <1us resolution.
#define USE_CLOCK_MONOTONIC 1
#else
// Use std::chrono::high_resolution_clock.
#include <chrono>
#define USE_CLOCK_MONOTONIC 0
#endif
#endif // USE_CLOCK_MONOTONIC

const char * utility_cpp_cvsid = "$Id: utility.cpp 5519 2023-07-24 15:57:54Z chrfranke $"
  UTILITY_H_CVSID;

const char * packet_types[] = {
        "Direct-access (disk)",
        "Sequential-access (tape)",
        "Printer",
        "Processor",
        "Write-once (optical disk)",
        "CD/DVD",
        "Scanner",
        "Optical memory (optical disk)",
        "Medium changer",
        "Communications",
        "Graphic arts pre-press (10)",
        "Graphic arts pre-press (11)",
        "Array controller",
        "Enclosure services",
        "Reduced block command (simplified disk)",
        "Optical card reader/writer"
};

// BUILD_INFO can be provided by package maintainers
#ifndef BUILD_INFO
#define BUILD_INFO "(local build)"
#endif

// Make version information string
// lines: 1: version only, 2: version+copyright, >=3: full information
std::string format_version_info(const char * prog_name, int lines /* = 2 */)
{
  std::string info = strprintf(
    "%s "
#ifndef SMARTMONTOOLS_RELEASE_DATE
      "pre-"
#endif
      PACKAGE_VERSION " "
#ifdef SMARTMONTOOLS_SVN_REV
      SMARTMONTOOLS_SVN_DATE " r" SMARTMONTOOLS_SVN_REV
#else
      "(build date " __DATE__ ")" // checkout without expansion of Id keywords
#endif
      " [%s] " BUILD_INFO "\n",
    prog_name, smi()->get_os_version_str().c_str()
  );
  if (lines <= 1)
    return info;

  info += "Copyright (C) 2002-23, Bruce Allen, Christian Franke, www.smartmontools.org\n";
  if (lines == 2)
    return info;

  info += "\n";
  info += prog_name;
  info += " comes with ABSOLUTELY NO WARRANTY. This is free\n"
    "software, and you are welcome to redistribute it under\n"
    "the terms of the GNU General Public License; either\n"
    "version 2, or (at your option) any later version.\n"
    "See https://www.gnu.org for further details.\n"
    "\n"
#ifndef SMARTMONTOOLS_RELEASE_DATE
    "smartmontools pre-release " PACKAGE_VERSION "\n"
#else
    "smartmontools release " PACKAGE_VERSION
      " dated " SMARTMONTOOLS_RELEASE_DATE " at " SMARTMONTOOLS_RELEASE_TIME "\n"
#endif
#ifdef SMARTMONTOOLS_SVN_REV
    "smartmontools SVN rev " SMARTMONTOOLS_SVN_REV
      " dated " SMARTMONTOOLS_SVN_DATE " at " SMARTMONTOOLS_SVN_TIME "\n"
#else
    "smartmontools SVN rev is unknown\n"
#endif
    "smartmontools build host: " SMARTMONTOOLS_BUILD_HOST "\n"
    "smartmontools build with: "

#define N2S_(s) #s
#define N2S(s) N2S_(s)
#if   __cplusplus == 202002
                               "C++20"
#elif __cplusplus == 201703
                               "C++17"
#elif __cplusplus == 201402
                               "C++14"
#elif __cplusplus == 201103
                               "C++11"
#else
                               "C++(" N2S(__cplusplus) ")"
#endif
#undef N2S
#undef N2S_

#if defined(__GNUC__) && defined(__VERSION__) // works also with CLang
                                     ", GCC " __VERSION__
#endif
#ifdef __MINGW64_VERSION_STR
                                     ", MinGW-w64 " __MINGW64_VERSION_STR
#endif
                                                          "\n"
    "smartmontools configure arguments:"
#ifdef SOURCE_DATE_EPOCH
                                      " [hidden in reproducible builds]\n"
    "reproducible build SOURCE_DATE_EPOCH: "
#endif
  ;
#ifdef SOURCE_DATE_EPOCH
  char ts[32]; struct tm tmbuf;
  strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", time_to_tm_local(&tmbuf, SOURCE_DATE_EPOCH));
  info += strprintf("%u (%s)", (unsigned)SOURCE_DATE_EPOCH, ts);
#else
  info += (sizeof(SMARTMONTOOLS_CONFIGURE_ARGS) > 1 ?
           SMARTMONTOOLS_CONFIGURE_ARGS : " [no arguments given]");
#endif
  info += '\n';

  return info;
}

// Solaris only: Get site-default timezone. This is called from
// UpdateTimezone() when TZ environment variable is unset at startup.
#if defined (__SVR4) && defined (__sun)
static const char *TIMEZONE_FILE = "/etc/TIMEZONE";

static char *ReadSiteDefaultTimezone(){
  FILE *fp;
  char buf[512], *tz;
  int n;

  tz = NULL;
  fp = fopen(TIMEZONE_FILE, "r");
  if(fp == NULL) return NULL;
  while(fgets(buf, sizeof(buf), fp)) {
    if (strncmp(buf, "TZ=", 3))    // searches last "TZ=" line
      continue;
    n = strlen(buf) - 1;
    if (buf[n] == '\n') buf[n] = 0;
    if (tz) free(tz);
    tz = strdup(buf);
  }
  fclose(fp);
  return tz;
}
#endif

// Make sure that this executable is aware if the user has changed the
// time-zone since the last time we polled devices. The canonical
// example is a user who starts smartd on a laptop, then flies across
// time-zones with a laptop, and then changes the timezone, WITHOUT
// restarting smartd. This is a work-around for a bug in
// GLIBC. Yuk. See bug number 48184 at http://bugs.debian.org and
// thanks to Ian Redfern for posting a workaround.

// Please refer to the smartd manual page, in the section labeled LOG
// TIMESTAMP TIMEZONE.
void FixGlibcTimeZoneBug(){
#if __GLIBC__  
  if (!getenv("TZ")) {
    putenv((char *)"TZ=GMT"); // POSIX prototype is 'int putenv(char *)'
    tzset();
    putenv((char *)"TZ");
    tzset();
  }
#elif _WIN32
  const char * tz = getenv("TZ");
  if (!tz) {
    putenv("TZ=GMT");
    tzset();
    putenv("TZ=");  // empty value removes TZ, putenv("TZ") does nothing
    tzset();
  }
  else {
    static const regular_expression tzrex("[A-Z]{3}[-+]?[0-9]+([A-Z]{3,4})?");
    // tzset() from MSVCRT only supports the above basic syntax of TZ.
    // Otherwise the timezone settings are set to bogus values.
    // Unset TZ and revert to system default timezone in these cases.
    if (!tzrex.full_match(tz)) {
      putenv("TZ=");
      tzset();
    }
  }
#elif defined (__SVR4) && defined (__sun)
  // In Solaris, putenv("TZ=") sets null string and invalid timezone.
  // putenv("TZ") does nothing.  With invalid TZ, tzset() do as if
  // TZ=GMT.  With TZ unset, /etc/TIMEZONE will be read only _once_ at
  // first tzset() call.  Conclusion: Unlike glibc, dynamic
  // configuration of timezone can be done only by changing actual
  // value of TZ environment value.
  enum tzstate { NOT_CALLED_YET, USER_TIMEZONE, TRACK_TIMEZONE };
  static enum tzstate state = NOT_CALLED_YET;

  static struct stat prev_stat;
  static char *prev_tz;
  struct stat curr_stat;
  char *curr_tz;

  if(state == NOT_CALLED_YET) {
    if(getenv("TZ")) {
      state = USER_TIMEZONE; // use supplied timezone
    } else {
      state = TRACK_TIMEZONE;
      if(stat(TIMEZONE_FILE, &prev_stat)) {
	state = USER_TIMEZONE;	// no TZ, no timezone file; use GMT forever
      } else {
	prev_tz = ReadSiteDefaultTimezone(); // track timezone file change
	if(prev_tz) putenv(prev_tz);
      }
    }
    tzset();
  } else if(state == TRACK_TIMEZONE) {
    if(stat(TIMEZONE_FILE, &curr_stat) == 0
       && (curr_stat.st_ctime != prev_stat.st_ctime
	    || curr_stat.st_mtime != prev_stat.st_mtime)) {
      // timezone file changed
      curr_tz = ReadSiteDefaultTimezone();
      if(curr_tz) {
	putenv(curr_tz);
	if(prev_tz) free(prev_tz);
	prev_tz = curr_tz; prev_stat = curr_stat; 
      }
    }
    tzset();
  }
#endif
  // OTHER OS/LIBRARY FIXES SHOULD GO HERE, IF DESIRED.  PLEASE TRY TO
  // KEEP THEM INDEPENDENT.
  return;
}

#ifdef _WIN32
// Fix strings in tzname[] to avoid long names with non-ascii characters.
// If TZ is not set, tzset() in the MSVC runtime sets tzname[] to the
// national language timezone names returned by GetTimezoneInformation().
static char * fixtzname(char * dest, int destsize, const char * src)
{
  int i = 0, j = 0;
  while (src[i] && j < destsize-1) {
    int i2 = (const char *)_mbsinc((const unsigned char *)src+i) - src;
    if (i2 > i+1)
      i = i2; // Ignore multibyte chars
    else {
      if ('A' <= src[i] && src[i] <= 'Z')
        dest[j++] = src[i]; // "Pacific Standard Time" => "PST"
      i++;
    }
  }
  if (j < 2)
    j = 0;
  dest[j] = 0;
  return dest;
}
#endif // _WIN32

// This value follows the peripheral device type value as defined in
// SCSI Primary Commands, ANSI INCITS 301:1997.  It is also used in
// the ATA standard for packet devices to define the device type.
const char *packetdevicetype(int type){
  if (type<0x10)
    return packet_types[type];
  
  if (type<0x20)
    return "Reserved";
  
  return "Unknown";
}

// Convert time to broken-down local time, throw on error.
struct tm * time_to_tm_local(struct tm * tp, time_t t)
{
#ifndef _WIN32
  // POSIX, C23 - missing in MSVRCT, C++ and older C.
  if (!localtime_r(&t, tp))
    throw std::runtime_error("localtime_r() failed");
#elif 0 // defined(__STDC_LIB_EXT1__)
  // C11 - requires #define __STDC_WANT_LIB_EXT1__ before <time.h>.
  // Missing in POSIX and C++, MSVCRT variant differs.
  if (!localtime_s(&t, tp))
    throw std::runtime_error("localtime_s() failed");
#else
  // MSVCRT - 64-bit variant avoids conflict with the above C11 variant.
  __time64_t t64 = t;
  if (_localtime64_s(tp, &t64))
    throw std::runtime_error("_localtime64_s() failed");
#endif
  return tp;
}

// Utility function prints date and time and timezone into a character
// buffer of length 64.  All the fuss is needed to get the right
// timezone info (sigh).
void dateandtimezoneepoch(char (& buffer)[DATEANDEPOCHLEN], time_t tval)
{
  FixGlibcTimeZoneBug();
  
  // Get the time structure.  We need this to determine if we are in
  // daylight savings time or not.
  struct tm tmbuf, * tmval = time_to_tm_local(&tmbuf, tval);
  
  // Convert to an ASCII string, put in datebuffer.
  // Same as: strftime(datebuffer, sizeof(datebuffer), "%a %b %e %H:%M:%S %Y\n"),
  // but always in "C" locale.
  char datebuffer[32];
  STATIC_ASSERT(sizeof(datebuffer) >= 26); // assumed by asctime_r()
#ifndef _WIN32
  // POSIX (missing in MSVRCT, C and C++)
  if (!asctime_r(tmval, datebuffer))
    throw std::runtime_error("asctime_r() failed");
#else
  // MSVCRT, C11 (missing in POSIX)
  if (asctime_s(datebuffer, sizeof(datebuffer), tmval))
    throw std::runtime_error("asctime_s() failed");
#endif
  
  // Remove newline
  int lenm1 = strlen(datebuffer) - 1;
  datebuffer[lenm1>=0?lenm1:0]='\0';

#if defined(_WIN32) && defined(_MSC_VER)
  // tzname is missing in MSVC14
  #define tzname _tzname
#endif

  // correct timezone name
  const char * timezonename;
  if (tmval->tm_isdst==0)
    // standard time zone
    timezonename=tzname[0];
  else if (tmval->tm_isdst>0)
    // daylight savings in effect
    timezonename=tzname[1];
  else
    // unable to determine if daylight savings in effect
    timezonename="";

#ifdef _WIN32
  // Fix long non-ascii timezone names
    // cppcheck-suppress variableScope
  char tzfixbuf[6+1] = "";
  if (!getenv("TZ"))
    timezonename=fixtzname(tzfixbuf, sizeof(tzfixbuf), timezonename);
#endif
  
  // Finally put the information into the buffer as needed.
  snprintf(buffer, DATEANDEPOCHLEN, "%s %s", datebuffer, timezonename);
  
  return;
}

// A replacement for perror() that sends output to our choice of
// printing. If errno not set then just print message.
void syserror(const char *message){
  
  if (errno) {
    // Get the correct system error message:
    const char *errormessage=strerror(errno);
    
    // Check that caller has handed a sensible string, and provide
    // appropriate output. See perror(3) man page to understand better.
    if (message && *message)
      pout("%s: %s\n",message, errormessage);
    else
      pout("%s\n",errormessage);
  }
  else if (message && *message)
    pout("%s\n",message);
  
  return;
}

// Check regular expression for non-portable features.
//
// POSIX extended regular expressions interpret unmatched ')' ordinary:
// "The close-parenthesis shall be considered special in this context
//  only if matched with a preceding open-parenthesis."
//
// GNU libc and BSD libc support unmatched ')', Cygwin reports an error.
//
// POSIX extended regular expressions do not define empty subexpressions:
// "A vertical-line appearing first or last in an ERE, or immediately following
//  a vertical-line or a left-parenthesis, or immediately preceding a
//  right-parenthesis, produces undefined results."
//
// GNU libc and Cygwin support empty subexpressions, BSD libc reports an error.
//
static const char * check_regex(const char * pattern)
{
  int level = 0;
  char c;

  for (int i = 0; (c = pattern[i]); i++) {
    // Skip "\x"
    if (c == '\\') {
      if (!pattern[++i])
        break;
      continue;
    }

    // Skip "[...]"
    if (c == '[') {
      if (pattern[++i] == '^')
        i++;
      if (!pattern[i++])
        break;
      while ((c = pattern[i]) && c != ']')
        i++;
      if (!c)
        break;
      continue;
    }

    // Check "(...)" nesting
    if (c == '(')
      level++;
    else if (c == ')' && --level < 0)
      return "Unmatched ')'";

    // Check for leading/trailing '|' or "||", "|)", "|$", "(|", "^|"
    char c1;
    if (   (c == '|' && (   i == 0 || !(c1 = pattern[i+1])
                          || c1 == '|' || c1 == ')' || c1 == '$'))
        || ((c == '(' || c == '^') && pattern[i+1] == '|')       )
      return "Empty '|' subexpression";
  }

  return (const char *)0;
}

// Wrapper class for POSIX regex(3) or std::regex

#ifndef WITH_CXX11_REGEX

regular_expression::regular_expression()
{
  memset(&m_regex_buf, 0, sizeof(m_regex_buf));
}

regular_expression::~regular_expression()
{
  free_buf();
}

regular_expression::regular_expression(const regular_expression & x)
: m_pattern(x.m_pattern),
  m_errmsg(x.m_errmsg)
{
  memset(&m_regex_buf, 0, sizeof(m_regex_buf));
  copy_buf(x);
}

regular_expression & regular_expression::operator=(const regular_expression & x)
{
  m_pattern = x.m_pattern;
  m_errmsg = x.m_errmsg;
  free_buf();
  copy_buf(x);
  return *this;
}

void regular_expression::free_buf()
{
  if (nonempty(&m_regex_buf, sizeof(m_regex_buf))) {
    regfree(&m_regex_buf);
    memset(&m_regex_buf, 0, sizeof(m_regex_buf));
  }
}

void regular_expression::copy_buf(const regular_expression & x)
{
  if (nonempty(&x.m_regex_buf, sizeof(x.m_regex_buf))) {
    // There is no POSIX compiled-regex-copy command.
    if (!compile())
      throw std::runtime_error(strprintf(
        "Unable to recompile regular expression \"%s\": %s",
        m_pattern.c_str(), m_errmsg.c_str()));
  }
}

#endif // !WITH_CXX11_REGEX

regular_expression::regular_expression(const char * pattern)
: m_pattern(pattern)
{
  if (!compile())
    throw std::runtime_error(strprintf(
      "error in regular expression \"%s\": %s",
      m_pattern.c_str(), m_errmsg.c_str()));
}

bool regular_expression::compile(const char * pattern)
{
#ifndef WITH_CXX11_REGEX
  free_buf();
#endif
  m_pattern = pattern;
  return compile();
}

bool regular_expression::compile()
{
#ifdef WITH_CXX11_REGEX
  try {
    m_regex.assign(m_pattern, std::regex_constants::extended);
  }
  catch (std::regex_error & ex) {
    m_errmsg = ex.what();
    return false;
  }

#else
  int errcode = regcomp(&m_regex_buf, m_pattern.c_str(), REG_EXTENDED);
  if (errcode) {
    char errmsg[512];
    regerror(errcode, &m_regex_buf, errmsg, sizeof(errmsg));
    m_errmsg = errmsg;
    free_buf();
    return false;
  }
#endif

  const char * errmsg = check_regex(m_pattern.c_str());
  if (errmsg) {
    m_errmsg = errmsg;
#ifdef WITH_CXX11_REGEX
    m_regex = std::regex();
#else
    free_buf();
#endif
    return false;
  }

  m_errmsg.clear();
  return true;
}

bool regular_expression::full_match(const char * str) const
{
#ifdef WITH_CXX11_REGEX
  return std::regex_match(str, m_regex);
#else
  match_range range;
  return (   !regexec(&m_regex_buf, str, 1, &range, 0)
          && range.rm_so == 0 && range.rm_eo == (int)strlen(str));
#endif
}

bool regular_expression::execute(const char * str, unsigned nmatch, match_range * pmatch) const
{
#ifdef WITH_CXX11_REGEX
  std::cmatch m;
  if (!std::regex_search(str, m, m_regex))
    return false;
  unsigned sz = m.size();
  for (unsigned i = 0; i < nmatch; i++) {
    if (i < sz && *m[i].first) {
      pmatch[i].rm_so = m[i].first  - str;
      pmatch[i].rm_eo = m[i].second - str;
    }
    else
      pmatch[i].rm_so = pmatch[i].rm_eo = -1;
  }
  return true;

#else
  return !regexec(&m_regex_buf, str, nmatch, pmatch, 0);
#endif
}

// Splits an argument to the -t option that is assumed to be of the form
// "selective,%lld-%lld" (prefixes of "0" (for octal) and "0x"/"0X" (for hex)
// are allowed).  The first long long int is assigned to *start and the second
// to *stop.  Returns zero if successful and non-zero otherwise.
int split_selective_arg(char *s, uint64_t *start,
                        uint64_t *stop, int *mode)
{
  char *tailptr;
  if (!(s = strchr(s, ',')))
    return 1;
  bool add = false;
  if (!isdigit((int)(*++s))) {
    *start = *stop = 0;
    if (!strncmp(s, "redo", 4))
      *mode = SEL_REDO;
    else if (!strncmp(s, "next", 4))
      *mode = SEL_NEXT;
    else if (!strncmp(s, "cont", 4))
      *mode = SEL_CONT;
    else
      return 1;
    s += 4;
    if (!*s)
      return 0;
    if (*s != '+')
      return 1;
  }
  else {
    *mode = SEL_RANGE;
    errno = 0;
    // Last argument to strtoull (the base) is 0 meaning that decimal is assumed
    // unless prefixes of "0" (for octal) or "0x"/"0X" (for hex) are used.
    *start = strtoull(s, &tailptr, 0);
    s = tailptr;
    add = (*s == '+');
    if (!(!errno && (add || *s == '-')))
      return 1;
    if (!strcmp(s, "-max")) {
      *stop = ~(uint64_t)0; // replaced by max LBA later
      return 0;
    }
  }

  errno = 0;
  *stop = strtoull(s+1, &tailptr, 0);
  if (errno || *tailptr != '\0')
    return 1;
  if (add) {
    if (*stop > 0)
      (*stop)--;
    *stop += *start; // -t select,N+M => -t select,N,(N+M-1)
  }
  return 0;
}

// Returns true if region of memory contains non-zero entries
bool nonempty(const void * data, int size)
{
  for (int i = 0; i < size; i++)
    if (((const unsigned char *)data)[i])
      return true;
  return false;
}

// Copy not null terminated char array to null terminated string.
// Replace non-ascii characters.  Remove leading and trailing blanks.
const char * format_char_array(char * str, int strsize, const char * chr, int chrsize)
{
  int b = 0;
  while (b < chrsize && chr[b] == ' ')
    b++;
  int n = 0;
  while (b+n < chrsize && chr[b+n])
    n++;
  while (n > 0 && chr[b+n-1] == ' ')
    n--;

  if (n >= strsize)
    n = strsize-1;

  for (int i = 0; i < n; i++) {
    char c = chr[b+i];
    str[i] = (' ' <= c && c <= '~' ? c : '?');
  }

  str[n] = 0;
  return str;
}

// Format integer with thousands separator
const char * format_with_thousands_sep(char * str, int strsize, uint64_t val,
                                       const char * thousands_sep /* = 0 */)
{
  if (!thousands_sep) {
    thousands_sep = ",";
#ifdef HAVE_LOCALE_H
    setlocale(LC_ALL, "");
    const struct lconv * currentlocale = localeconv();
    if (*(currentlocale->thousands_sep))
      thousands_sep = currentlocale->thousands_sep;
#endif
  }

  char num[64];
  snprintf(num, sizeof(num), "%" PRIu64, val);
  int numlen = strlen(num);

  int i = 0, j = 0;
  do
    str[j++] = num[i++];
  while (i < numlen && (numlen - i) % 3 != 0 && j < strsize-1);
  str[j] = 0;

  while (i < numlen && j < strsize-1) {
    j += snprintf(str+j, strsize-j, "%s%.3s", thousands_sep, num+i);
    i += 3;
  }

  return str;
}

// Format capacity with SI prefixes
const char * format_capacity(char * str, int strsize, uint64_t val,
                             const char * decimal_point /* = 0 */)
{
  if (!decimal_point) {
    decimal_point = ".";
#ifdef HAVE_LOCALE_H
    setlocale(LC_ALL, "");
    const struct lconv * currentlocale = localeconv();
    if (*(currentlocale->decimal_point))
      decimal_point = currentlocale->decimal_point;
#endif
  }

  const unsigned factor = 1000; // 1024 for KiB,MiB,...
  static const char prefixes[] = " KMGTP";

  // Find d with val in [d, d*factor)
  unsigned i = 0;
  uint64_t d = 1;
  for (uint64_t d2 = d * factor; val >= d2; d2 *= factor) {
    d = d2;
    if (++i >= sizeof(prefixes)-2)
      break;
  }

  // Print 3 digits
  uint64_t n = val / d;
  if (i == 0)
    snprintf(str, strsize, "%u B", (unsigned)n);
  else if (n >= 100) // "123 xB"
    snprintf(str, strsize, "%" PRIu64 " %cB", n, prefixes[i]);
  else if (n >= 10)  // "12.3 xB"
    snprintf(str, strsize, "%" PRIu64 "%s%u %cB", n, decimal_point,
        (unsigned)(((val % d) * 10) / d), prefixes[i]);
  else               // "1.23 xB"
    snprintf(str, strsize, "%" PRIu64 "%s%02u %cB", n, decimal_point,
        (unsigned)(((val % d) * 100) / d), prefixes[i]);

  return str;
}

// return (v)sprintf() formatted std::string
__attribute_format_printf(1, 0)
std::string vstrprintf(const char * fmt, va_list ap)
{
  char buf[512];
  vsnprintf(buf, sizeof(buf), fmt, ap);
  buf[sizeof(buf)-1] = 0;
  return buf;
}

std::string strprintf(const char * fmt, ...)
{
  va_list ap; va_start(ap, fmt);
  std::string str = vstrprintf(fmt, ap);
  va_end(ap);
  return str;
}

#if defined(HAVE___INT128)
// Compiler supports '__int128'.

// Recursive 128-bit to string conversion function
static int snprint_uint128(char * str, int strsize, unsigned __int128 value)
{
  if (strsize <= 0)
    return -1;

  if (value <= 0xffffffffffffffffULL) {
    // Print leading digits as 64-bit value
    return snprintf(str, (size_t)strsize, "%" PRIu64, (uint64_t)value);
  }
  else {
    // Recurse to print leading digits
    const uint64_t e19 = 10000000000000000000ULL; // 2^63 < 10^19 < 2^64
    int len1 = snprint_uint128(str, strsize, value / e19);
    if (len1 < 0)
      return -1;

    // Print 19 digits remainder as 64-bit value
    int len2 = snprintf(str + (len1 < strsize ? len1 : strsize - 1),
                        (size_t)(len1 < strsize ? strsize - len1 : 1),
                        "%019" PRIu64, (uint64_t)(value % e19)        );
    if (len2 < 0)
      return -1;
    return len1 + len2;
  }
}

// Convert 128-bit unsigned integer provided as two 64-bit halves to a string.
const char * uint128_hilo_to_str(char * str, int strsize, uint64_t value_hi, uint64_t value_lo)
{
  snprint_uint128(str, strsize, ((unsigned __int128)value_hi << 64) | value_lo);
  return str;
}

#elif defined(HAVE_LONG_DOUBLE_WIDER_PRINTF)
// Compiler and *printf() support 'long double' which is wider than 'double'.

const char * uint128_hilo_to_str(char * str, int strsize, uint64_t value_hi, uint64_t value_lo)
{
  snprintf(str, strsize, "%.0Lf", value_hi * (0xffffffffffffffffULL + 1.0L) + value_lo);
  return str;
}

#else // !HAVE_LONG_DOUBLE_WIDER_PRINTF
// No '__int128' or 'long double' support, use 'double'.

const char * uint128_hilo_to_str(char * str, int strsize, uint64_t value_hi, uint64_t value_lo)
{
  snprintf(str, strsize, "%.0f", value_hi * (0xffffffffffffffffULL + 1.0) + value_lo);
  return str;
}

#endif // HAVE___INT128

// Get microseconds since some unspecified starting point.
long long get_timer_usec()
{
#if USE_CLOCK_MONOTONIC
  struct timespec ts;
  if (clock_gettime(CLOCK_MONOTONIC, &ts))
    return -1;
  return ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
#else
  return std::chrono::duration_cast<std::chrono::microseconds>(
    std::chrono::high_resolution_clock::now().time_since_epoch()
  ).count();
#endif
}

// Runtime check of byte ordering, throws on error.
static void check_endianness()
{
  const union {
    // Force compile error if int type is not 32bit.
    unsigned char c[sizeof(int) == 4 ? 8 : -1];
    uint64_t i;
  } x = {{1, 2, 3, 4, 5, 6, 7, 8}};
  const uint64_t le = 0x0807060504030201ULL;
  const uint64_t be = 0x0102030405060708ULL;

  if (!(   x.i == (isbigendian() ? be : le)
        && sg_get_unaligned_le16(x.c)   == (uint16_t)le
        && sg_get_unaligned_be16(x.c+6) == (uint16_t)be
        && sg_get_unaligned_le32(x.c)   == (uint32_t)le
        && sg_get_unaligned_be32(x.c+4) == (uint32_t)be
        && sg_get_unaligned_le64(x.c)   == le
        && sg_get_unaligned_be64(x.c)   == be          ))
    throw std::logic_error("CPU endianness does not match compile time test");
}

#if defined(__GNUC__) && (__GNUC__ >= 7)

// G++ 7+: Assume sane implementation and avoid -Wformat-truncation warning
static void check_snprintf() {}

#else

static void check_snprintf()
{
  char buf[] =              "ABCDEFGHI";
  int n1 = snprintf(buf, 8, "123456789");
  int n2 = snprintf(buf, 0, "X");
  if (!(!strcmp(buf, "1234567") && n1 == 9 && n2 == 1))
    throw std::logic_error("Function snprintf() does not conform to C99");
}

#endif

// Runtime check of ./configure result, throws on error.
void check_config()
{
  check_endianness();
  check_snprintf();
}
