/* The probe that stands in for every command on the Shell Expansion page.
 *
 * The page never runs a program. What it claims is what a program would be
 * handed: an argument vector and a table of open file descriptors. So the
 * oracle (scripts/shell-expansion-oracle.py) runs real dash and bash with this
 * probe installed under each command name the page offers, and the probe
 * writes down exactly those two things before it does anything else.
 *
 * For each open descriptor it records the file it names (/proc/self/fd), the
 * access mode and append flag (/proc/self/fdinfo), and which open file
 * description it shares with which lower descriptor, using kcmp(2). That last
 * one is what separates `>o 2>&1` (one description, one offset) from
 * `>o 2>o` (two of them), which a file name alone cannot.
 *
 * Output: one JSON line appended to the file named by $PROBE_OUT, written with
 * a single write(2) so that two probes in one pipeline cannot interleave. The
 * report file is opened only after the table has been read, so it never
 * appears in it.
 *
 *   cc -O2 -o probe scripts/shell-expansion-probe.c
 */
#define _GNU_SOURCE
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>

#ifndef KCMP_FILE
#define KCMP_FILE 0
#endif

static char out[65536];
static size_t used;

static void put(const char *s) {
  size_t n = strlen(s);
  if (used + n < sizeof out) { memcpy(out + used, s, n); used += n; }
}

static void putjson(const char *s) {
  char b[8];
  put("\"");
  for (; *s; s++) {
    unsigned char c = (unsigned char)*s;
    if (c == '"' || c == '\\') { b[0] = '\\'; b[1] = c; b[2] = 0; put(b); }
    else if (c < 0x20) { snprintf(b, sizeof b, "\\u%04x", c); put(b); }
    else { b[0] = c; b[1] = 0; put(b); }
  }
  put("\"");
}

int main(int argc, char **argv) {
  int fds[64], n = 0;
  /* Read the table first, before this process opens anything of its own. The
   * directory stream is itself a descriptor, so it is noted and left out. */
  DIR *d = opendir("/proc/self/fd");
  int self = d ? dirfd(d) : -1;
  struct dirent *e;
  while (d && (e = readdir(d))) {
    if (e->d_name[0] == '.') continue;
    int fd = atoi(e->d_name);
    if (fd != self && n < 64) fds[n++] = fd;
  }
  /* Sort, so the report lists descriptors in order. */
  for (int i = 1; i < n; i++)
    for (int j = i; j > 0 && fds[j - 1] > fds[j]; j--) {
      int t = fds[j]; fds[j] = fds[j - 1]; fds[j - 1] = t;
    }

  char path[64], target[4096], info[4096];
  /* The pid orders the members of a pipeline: both shells fork them left to
   * right, and the oracle checks that order against the pipe ends. */
  snprintf(path, sizeof path, "{\"pid\":%d,\"argv\":[", (int)getpid());
  put(path);
  for (int i = 0; i < argc; i++) { if (i) put(","); putjson(argv[i]); }
  put("],\"fds\":[");
  pid_t me = getpid();
  for (int i = 0; i < n; i++) {
    int fd = fds[i];
    snprintf(path, sizeof path, "/proc/self/fd/%d", fd);
    ssize_t k = readlink(path, target, sizeof target - 1);
    target[k < 0 ? 0 : k] = 0;
    snprintf(path, sizeof path, "/proc/self/fdinfo/%d", fd);
    long flags = -1;
    FILE *f = fopen(path, "r");
    if (f) {
      while (fgets(info, sizeof info, f))
        if (!strncmp(info, "flags:", 6)) flags = strtol(info + 6, NULL, 8);
      fclose(f);
    }
    /* The lowest descriptor that shares this one's open file description. */
    int same = fd;
    for (int j = 0; j < i; j++) {
      long r = syscall(SYS_kcmp, me, me, KCMP_FILE, fds[j], fd);
      if (r == 0) { same = fds[j]; break; }
    }
    char line[64];
    if (i) put(",");
    snprintf(line, sizeof line, "{\"fd\":%d,\"target\":", fd);
    put(line);
    putjson(target);
    const char *mode = flags < 0 ? "?" : (flags & 3) == 0 ? "r"
                     : (flags & 3) == 1 ? "w" : "rw";
    snprintf(line, sizeof line, ",\"mode\":\"%s\",\"append\":%s,\"shares\":%d}",
             mode, (flags >= 0 && (flags & O_APPEND)) ? "true" : "false", same);
    put(line);
  }
  put("]}\n");
  if (d) closedir(d);

  const char *dest = getenv("PROBE_OUT");
  if (!dest) return 2;
  int o = open(dest, O_WRONLY | O_APPEND | O_CREAT, 0644);
  if (o < 0) return 3;
  if (write(o, out, used) != (ssize_t)used) return 4;
  close(o);
  return 0;
}
