[
  {
    "path": ".gitignore",
    "content": "/hexd\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## 1.1.0\n  * default to colours/formatting based on whether output is a TTY\n  * add verbose option to show all bytes (and not omit repeated lines)\n  * add -h as an option to show usage\n  * fix misaligned output in case -w width doesn't divide BUFSIZ\n  * fix downcasting issue from `off_t` to a potentially smaller size\n\n## 1.0.0\n  initial release\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (C) 2013 Jonas ‘FireFly’ Höglund\n\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "CFLAGS += -Wall -std=c11\n\nPREFIX ?= /usr/local\nBINDIR ?= $(PREFIX)/bin\nMANDIR ?= $(PREFIX)/share/man\n\n.PHONY: all\nall: hexd\n\n.PHONY: clean\nclean:\n\trm -f hexd\n\n.PHONY: install\ninstall: hexd\n\tmkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1\n\tinstall hexd $(DESTDIR)$(BINDIR)/hexd\n\tinstall hexd.1 $(DESTDIR)$(MANDIR)/man1/hexd.1\n"
  },
  {
    "path": "README.md",
    "content": "**hexd** prints a human-readable hexdump of the specified files, or standard\ninput if omitted.  Its main distinguishing feature is the use of colours to\nvisually indicate which range of values an octet belongs to, aiding in\nspotting patterns in binary data.\n\nBy default, *hexd* relies on 256-color SGR escape sequences.  Most terminal\nemulators should support these today, but technically they're only defacto\nstandard.  However, you can override the formatting used with the\n`HEXD_COLORS` environment variable (see manpage), or use the `-p` option for\nplaintext output.\n\n## Screenshot\n![Screenshot](meta/screenshot.png \"Output after being run on an ELF binary\")\n\n## License\nMIT license.\n\n## See also\n* [pixd](http://github.com/FireyFly/pixd):\n  like *hexd*, but visualizes the data fed to it using a 256-colour palette\n  and half-block (\"▀\") characters.\n"
  },
  {
    "path": "hexd.1",
    "content": ".Dd February 18, 2022\n.Dt HEXD 1\n.Os\n.Sh NAME\n.Nm hexd\n.Nd human-friendly hexdump tool\n.Sh SYNOPSIS\n.Nm\n.Op Fl p\n.Op Fl P\n.Op Fl v\n.Op Fl g Ar groupsize\n.Op Fl r Ar range\n.Op Fl w Ar width\n.Op Ar\n.Sh DESCRIPTION\n.Nm\nprints a human-readable hexdump of the specified files, or standard input if\nomitted.  Its main distinguishing feature is the use of colours to visually\nindicate which range of values an octet belongs to, aiding in spotting\npatterns in binary data.\n.Pp\nThe ranges an octet is classified into are\n.Em zero\n.Li ( 0x00 ) ,\n.Em low\n.Li ( 0x01..0x1F ) ,\n.Em printable\n.Li ( 0x20..0x7E ) ,\n.Em high\n.Li ( 0x7F..0xFE )\nand\n.Em all\n.Li ( 0xFF ) .\n.Pp\nBy default, colours are used if output is a terminal, and omitted if not.\n.Pp\nRepeated output lines are collapsed into only one copy followed by a line with\nonly '*', unless\n.Fl v\nis used.\n.Sh OPTIONS\nIf no\n.Ar file\noperands are specified, standard input is read instead.  Available options are\nlisted below.\n.Bl -tag -width Ds\n.It Fl h\nPrint usage information.\n.It Fl p\nPlain: disable colours/formatting.\n.It Fl P\nPretty: enable colours/formatting.\n.It Fl v\nVerbose: show every hexdump line (don't collapse repetition with '*').\n.It Fl g Ar groupsize\nNumber of octets per group, set to\n.Li 8\nby default.\n.It Fl r Ar range\nRange of octets to print from each file.\nSpecified as either\n.Em start-end\nor\n.Em start+count ,\nwhere\n.Em start\nand\n.Em end Ns / Ns Em count\nare positive integers specified in either decimal, hexadecimal or octal\n(C-style notation).\n.Pp\nWhen the former syntax is used, both ends of the range are optional and\ndefault to the start or end of the file when omitted.\n.It Fl w Ar width\nNumber of octets per line, separated into groups (see\n.Fl g ) .\nSet to\n.Li 16\nby default.\n.El\n.Sh ENVIRONMENT\n.Ev HEXD_COLORS\ncan be used to override the formatting used by\n.Nm\nto classify octets.  If set, it should consist of space-separated pairs of the\nform\n.Em key=value ,\nwhere\n.Em key\nis one of 'zero', 'low', 'printable', 'high' or 'all', and\n.Em value\nis an SGR formatting string.  SGR formatting is interpreted by your terminal\nemulator; consult its documentation or ECMA-48 for more details.\n.Pp\nFor example, the default formatting used when\n.Ev HEXD_COLORS\nis not defined corresponds to the value\n.Pp\n.D1 Em zero=38;5;238 low=38;5;150 high=38;5;141 all=38;5;167\n.Sh EXAMPLES\nHere are some examples of useful uses of hexd's features.\n.Bl -tag -width Ds\n.It Em hexd -r0x1000+0x200 foo.bin\nDisplay the 512-byte range in 'foo.bin' starting at offset 0x1000.  Useful\nwhen files contain other embedded files/formats at a certain location (e.g.\narchive files).\n.It Em hexd -r-0x10 *.bin\nShow the first 16 bytes of each of the *.bin files, with a heading above each\nfile (if more than one).  This is useful for example to compare headers of\nseveral samples of an unknown format.\n.It Em curl -s http://example.com | hexd -P | less -R\n.Nm\nworks as a filter, too.  For paging long hexdumps,\n.Xr less 1 Ns 's\n.Fl R\nflag is useful.\n.El\n.Sh SEE ALSO\n.Xr hexdump 1 ,\n.Xr od 1 ,\n.Xr xxd 1\n.Sh AUTHORS\nWritten by\n.An FireFly\n"
  },
  {
    "path": "hexd.c",
    "content": "#define _POSIX_C_SOURCE 200809L\n#include <ctype.h>\n#include <err.h>\n#include <inttypes.h>\n#include <stdbool.h>\n#include <stdio.h>\n#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n\n#define MIN(X,Y) ((X) < (Y)? (X) : (Y))\ntypedef uint8_t u8;\n\n//-- Options ----------------\nstruct offset_range { off_t start, end; };\n\nsize_t option_columns      = 16;\nsize_t option_groupsize    = 8;\nbool option_use_formatting = true;\nbool option_collapse_repetition = true;\nstruct offset_range option_range  = { 0, -1 };\n\nconst char *formatting_zero      = \"38;5;238\";\nconst char *formatting_all       = \"38;5;167\";\nconst char *formatting_low       = \"38;5;150\";\nconst char *formatting_high      = \"38;5;141\";\nconst char *formatting_printable = \"\";\n\n//-- Hexdump impl -----------\nconst char *format_of(int v) {\n  return v == 0x00?  formatting_zero\n       : v == 0xFF?  formatting_all\n       : v <  0x20?  formatting_low\n       : v >= 0x7F?  formatting_high\n       :             formatting_printable;\n}\n\nconst char *CHAR_AREA_HIGH_LUT[] = {\n  \"€\", \".\", \"‚\", \"ƒ\", \"„\", \"…\", \"†\", \"‡\", \"ˆ\", \"‰\", \"Š\", \"‹\", \"Œ\", \".\", \"Ž\", \".\",\n  \".\", \"‘\", \"’\", \"“\", \"”\", \"•\", \"–\", \"—\", \"˜\", \"™\", \"š\", \"›\", \"œ\", \".\", \"ž\", \"Ÿ\",\n  \".\", \"¡\", \"¢\", \"£\", \"¤\", \"¥\", \"¦\", \"§\", \"¨\", \"©\", \"ª\", \"«\", \"¬\", \".\", \"®\", \"¯\",\n  \"°\", \"±\", \"²\", \"³\", \"´\", \"µ\", \"¶\", \"·\", \"¸\", \"¹\", \"º\", \"»\", \"¼\", \"½\", \"¾\", \"¿\",\n  \"À\", \"Á\", \"Â\", \"Ã\", \"Ä\", \"Å\", \"Æ\", \"Ç\", \"È\", \"É\", \"Ê\", \"Ë\", \"Ì\", \"Í\", \"Î\", \"Ï\",\n  \"Ð\", \"Ñ\", \"Ò\", \"Ó\", \"Ô\", \"Õ\", \"Ö\", \"×\", \"Ø\", \"Ù\", \"Ú\", \"Û\", \"Ü\", \"Ý\", \"Þ\", \"ß\",\n  \"à\", \"á\", \"â\", \"ã\", \"ä\", \"å\", \"æ\", \"ç\", \"è\", \"é\", \"ê\", \"ë\", \"ì\", \"í\", \"î\", \"ï\",\n  \"ð\", \"ñ\", \"ò\", \"ó\", \"ô\", \"õ\", \"ö\", \"÷\", \"ø\", \"ù\", \"ú\", \"û\", \"ü\", \"ý\", \"þ\", \"ÿ\",\n};\n\nvoid hexdump(FILE *f, const char *filename) {\n  u8 line[option_columns];\n  u8 prev_line[option_columns];\n\n  bool first_line = true, printed_asterisk = false;\n\n  // Seek to start; fall back to a consuming loop for non-seekable files\n  if (fseeko(f, option_range.start, SEEK_SET) < 0) {\n    off_t remaining = option_range.start;\n    while (remaining != 0 && fgetc(f) != EOF) remaining--;\n    if (ferror(f)) err(1, \"(while seeking) %s\", filename);\n  }\n\n  for (off_t offset = option_range.start; offset < option_range.end || option_range.end == -1; offset += option_columns) {\n    off_t read = offset - option_range.start;\n    size_t n = fread(line, 1, option_columns, f);\n    if (n == 0) break;\n\n    // Contract repeated identical lines\n    if (!first_line\n          && option_collapse_repetition\n          && memcmp(line, prev_line, option_columns) == 0\n          && n == option_columns) {\n      if (!printed_asterisk) {\n        printf(\"%8s\\n\", \"*\");\n        printed_asterisk = true;\n      }\n      continue;\n    }\n    printed_asterisk = false;\n\n    // Offset\n    intmax_t offset = option_range.start + read;\n    printf(\"%5jx%03jx\", offset >> 12, offset & 0xFFF);\n\n    // Print hex area\n    const char *prev_fmt = NULL;\n    for (size_t j = 0; j < option_columns; j++) {\n      if (option_groupsize != 0 && j % option_groupsize == 0) printf(\" \");\n      if (j < n) {\n        const char *fmt = format_of(line[j]);\n        if (prev_fmt != fmt && option_use_formatting) printf(\"\\x1B[%sm\", fmt);\n        printf(\" %02x\", line[j]);\n        prev_fmt = fmt;\n      } else {\n        printf(\"   \");\n      }\n    }\n    putchar(' ');\n\n    // Print char area\n    for (size_t j = 0; j < option_columns; j++) {\n      if (option_groupsize != 0 && j % option_groupsize == 0) printf(\" \");\n      if (j < n) {\n        const char *fmt = format_of(line[j]);\n        if (prev_fmt != fmt && option_use_formatting) printf(\"\\x1B[%sm\", fmt);\n        if (line[j] >= 0x80) printf(\"%s\", CHAR_AREA_HIGH_LUT[line[j] - 0x80]);\n        else putchar(isprint(line[j])? line[j] : '.');\n        prev_fmt = fmt;\n      } else {\n        putchar(' ');\n      }\n    }\n    printf(\"%s\\n\", option_use_formatting? \"\\x1B[m\" : \"\");\n\n    memcpy(prev_line, line, n);\n    first_line = false;\n    if (n < option_columns) break;\n  }\n\n  if (ferror(f)) err(1, \"(while reading) %s\", filename);\n}\n\n//-- Entry point ------------\n/** Parses a range \"start-end\" (both ends optional) or \"start+size\" (neither\n *  optional) into a `struct offset_range` instance. */\nstruct offset_range parse_range(const char *str) {\n  struct offset_range res = { 0, -1 };\n  const char *first = str, *delim = str + strcspn(str, \"+-\"), *second = delim + 1;\n  if (*delim == '\\0') errx(1, \"no delimiter in range %s\", str);\n\n  char *end;\n  if (first != delim) {\n    res.start = strtoimax(first, &end, 0);\n    if (!isdigit(*first) || end != delim) errx(1, \"invalid start value in range %s\", str);\n  }\n  if (*second != '\\0') {\n    res.end = strtoimax(second, &end, 0);\n    if (!isdigit(*second) || *end != '\\0') errx(1, \"invalid end/size value in range %s\", str);\n  }\n\n  if (*delim == '+') {\n    if (first == delim) errx(1, \"start unspecified in range %s\", str);\n    if (*second == '\\0') errx(1, \"size unspecified in range %s\", str);\n    res.end += res.start;\n  }\n\n  if (res.end < res.start && res.end != -1) errx(1, \"end was less than start in range %s\", str);\n  return res;\n}\n\nint main(int argc, char *argv[]) {\n  // Default to colourful output if output is a TTY\n  option_use_formatting = isatty(1);\n\n  // Parse options\n  int opt;\n  while (opt = getopt(argc, argv, \"g:hpPr:w:v\"), opt != -1) {\n    switch (opt) {\n      case 'g': option_groupsize = atol(optarg); break;\n      case 'p': option_use_formatting = false; break;\n      case 'P': option_use_formatting = true; break;\n      case 'r': option_range = parse_range(optarg); break;\n      case 'v': option_collapse_repetition = false; break;\n      case 'w': option_columns = atol(optarg); break;\n      case 'h': // fall through\n      default:\n        fprintf(stderr, \"usage: hexd [-p] [-P] [-v] [-g groupsize] [-r range] [-w width]\\n\");\n        return 1;\n    }\n  }\n  argc -= optind;\n  argv += optind;\n\n  // Parse HEXD_COLORS\n  char *colors_var = getenv(\"HEXD_COLORS\");\n  if (colors_var != NULL) {\n    colors_var = strdup(colors_var);\n    if (colors_var == NULL) errx(1, \"strdup\");\n\n    for (char *p = colors_var, *token; token = strtok(p, \" \"), token != NULL; p = NULL) {\n      char *key = token, *value = strchr(token, '=');\n      if (value == NULL) warnx(\"no '=' found in HEXD_COLORS property '%s'\", p);\n      *value++ = '\\0';\n\n      if      (strcmp(key, \"zero\")      == 0) formatting_zero      = value;\n      else if (strcmp(key, \"low\")       == 0) formatting_low       = value;\n      else if (strcmp(key, \"printable\") == 0) formatting_printable = value;\n      else if (strcmp(key, \"high\")      == 0) formatting_high      = value;\n      else if (strcmp(key, \"all\")       == 0) formatting_all       = value;\n      else warnx(\"unknown HEXD_COLORS property '%s'\", key);\n    }\n  }\n\n  // Hexdump files\n  if (argc == 0) {\n    hexdump(stdin, \"(stdin)\");\n  } else {\n    for (int i = 0; i < argc; i++) {\n      FILE *f = fopen(argv[i], \"r\");\n      if (f == NULL) {\n        warn(\"%s\", argv[i]);\n        continue;\n      }\n\n      if (argc > 1) {\n        printf(\"%s====> %s%s%s <====\\n\", i > 0? \"\\n\" : \"\",\n                                         option_use_formatting? \"\\x1B[1m\" : \"\",\n                                         argv[i],\n                                         option_use_formatting? \"\\x1B[m\" : \"\");\n      }\n\n      hexdump(f, argv[i]);\n      fclose(f);\n    }\n  }\n\n  free(colors_var);\n  return 0;\n}\n"
  }
]