git.strcat.st

/strcat/diff.git/ - summarytreelogarchive

subject
init
commit
c42579f21aff585681743e2d612612c0fe41ce6c
date
2026-04-23T20:39:27Z
message
diff
 Makefile |    5 +
 diff.c   | 1541 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 1546 insertions(+)

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d24ea6b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+diff: diff.c
+	cc -std=c89 -pedantic -Wall -Wextra -O2 diff.c -o diff
+
+clean:
+	rm -f diff
diff --git a/diff.c b/diff.c
new file mode 100644
index 0000000..044d5f4
--- /dev/null
+++ b/diff.c
@@ -0,0 +1,1541 @@
+/*
+ * POSIX IEEE Std 1003.1-2017 diff implementation
+ * with the following options;
+ * -b, -c, -C n, -e, -f, -r, -u, -U n
+ * public domain
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define	EXIT_SAME	0
+#define	EXIT_DIFF	1
+#define	EXIT_ERR	2
+
+enum fmt_kind {
+	FMT_DEFAULT,
+	FMT_CONTEXT,
+	FMT_UNIFIED,
+	FMT_ED,
+	FMT_FORWARD
+};
+
+struct options {
+	enum fmt_kind	fmt;
+	int		context;
+	int		ignore_space;
+	int		recursive;
+};
+
+struct file_data {
+	char		*bytes;
+	char		**lines;
+	char		*path;
+	size_t		len;
+	struct stat	st;
+	int		has_stat;
+	int		is_text;
+	int		line_count;
+};
+
+struct hunk {
+	int	a0;
+	int	a1;
+	int	b0;
+	int	b1;
+};
+
+struct hunk_list {
+	struct hunk	*v;
+	int		cap;
+	int		n;
+};
+
+struct str_list {
+	char	**v;
+	int	cap;
+	int	n;
+};
+
+struct inode_ref {
+	dev_t	dev;
+	ino_t	ino;
+};
+
+struct inode_stack {
+	struct inode_ref	*v;
+	int			cap;
+	int			n;
+};
+
+static const char	*progname = "diff";
+
+static int	build_file_hunks(const struct file_data *, const struct file_data *,
+	        const struct options *, struct hunk_list *);
+static int	compare_dirs(const char *, const char *, const char *, const char *,
+	        const struct options *, struct inode_stack *);
+static int	compare_file_paths(const char *, const char *, const char *, const char *,
+	        const struct options *, int);
+static int	compare_text_files(const struct file_data *, const struct file_data *,
+	        const struct options *, int);
+static int	ftype_rank(mode_t);
+static void	emit_context(const struct file_data *, const struct file_data *,
+	        const struct hunk_list *, int);
+static void	emit_default(const struct file_data *, const struct file_data *,
+	        const struct hunk_list *);
+static void	emit_ed(const struct file_data *, const struct file_data *,
+	        const struct hunk_list *);
+static void	emit_forward(const struct file_data *, const struct file_data *,
+	        const struct hunk_list *);
+static void	emit_unified(const struct file_data *, const struct file_data *,
+	        const struct hunk_list *, int);
+static void	file_to_lines(struct file_data *);
+static void	fmt_context_time(const struct file_data *, char *, size_t);
+static void	fmt_unified_time(const struct file_data *, char *, size_t);
+static void	free_file_data(struct file_data *);
+static void	free_str_list(struct str_list *);
+static const char	*ftype_name(mode_t);
+static void	hunk_push(struct hunk_list *, int, int, int, int);
+static int	inode_stack_contains(const struct inode_stack *, dev_t, ino_t);
+static void	inode_stack_pop(struct inode_stack *);
+static void	inode_stack_push(struct inode_stack *, dev_t, ino_t);
+static int	is_dir_path(const char *, struct stat *);
+static int	is_space_no_nl(int);
+static int	is_text_bytes(const char *, size_t);
+static char	*join_path(const char *, const char *);
+static int	line_equal(const char *, const char *, const struct options *);
+static int	line_equal_ws(const char *, const char *);
+static void	make_diffopt_string(const struct options *, char *, size_t);
+static int	name_cmp(const void *, const void *);
+static void	print_context_old_range(int, int);
+static void	print_context_new_range(int, int);
+static void	print_default_range(int, int);
+static void	print_ed_payload(const struct file_data *, int, int);
+static void	print_fwd_addr(int, int);
+static void	print_line_prefixed(const char *, const char *);
+static void	print_unified_range_part(int, int);
+static int	parse_nonneg(const char *, int *);
+static int	parse_options(int, char **, struct options *, int *);
+static int	read_dir_names(const char *, struct str_list *);
+static int	read_file_bytes(FILE *, char **, size_t *);
+static void	read_operand(const char *, struct file_data *);
+static void	usage(void) __attribute__((__noreturn__));
+static void	*xmalloc(size_t);
+static void	*xrealloc(void *, size_t);
+static char	*xstrdup(const char *);
+
+static void
+usage(void)
+{
+	fprintf(stderr,
+	    "usage: %s [-befr] [-c | -u | -e | -f] [-C n | -U n] file1 file2\n",
+	    progname);
+	exit(EXIT_ERR);
+}
+
+static void *
+xmalloc(size_t n)
+{
+	void	*p;
+
+	p = malloc(n);
+	if (p == NULL)
+	    errx(EXIT_ERR, "out of memory");
+	return p;
+}
+
+static void *
+xrealloc(void *p, size_t n)
+{
+	void	*q;
+
+	q = realloc(p, n);
+	if (q == NULL)
+	    errx(EXIT_ERR, "out of memory");
+	return q;
+}
+
+static char *
+xstrdup(const char *s)
+{
+	size_t	n;
+	char	*p;
+
+	n = strlen(s);
+	p = xmalloc(n + 1);
+	memcpy(p, s, n + 1);
+	return p;
+}
+
+static int
+parse_nonneg(const char *s, int *out)
+{
+	char	*end;
+	long	v;
+
+	if (s == NULL || *s == '\0')
+	    return 0;
+	errno = 0;
+	v = strtol(s, &end, 10);
+	if (errno != 0 || *end != '\0' || v < 0 || v > 2147483647L)
+	    return 0;
+	*out = (int)v;
+	return 1;
+}
+
+static int
+is_space_no_nl(int c)
+{
+	return c == ' ' || c == '\t' || c == '\v' || c == '\f' || c == '\r';
+}
+
+static const char *
+ftype_name(mode_t m)
+{
+	if (S_ISREG(m))
+	    return "regular file";
+	if (S_ISDIR(m))
+	    return "directory";
+	if (S_ISCHR(m))
+	    return "character special file";
+	if (S_ISBLK(m))
+	    return "block special file";
+	if (S_ISFIFO(m))
+	    return "fifo";
+#ifdef S_ISLNK
+	if (S_ISLNK(m))
+	    return "symbolic link";
+#endif
+#ifdef S_ISSOCK
+	if (S_ISSOCK(m))
+	    return "socket";
+#endif
+	return "file";
+}
+
+static int
+ftype_rank(mode_t m)
+{
+	if (S_ISREG(m))
+	    return 0;
+	if (S_ISDIR(m))
+	    return 1;
+	if (S_ISCHR(m))
+	    return 2;
+	if (S_ISBLK(m))
+	    return 3;
+	if (S_ISFIFO(m))
+	    return 4;
+#ifdef S_ISLNK
+	if (S_ISLNK(m))
+	    return 5;
+#endif
+#ifdef S_ISSOCK
+	if (S_ISSOCK(m))
+	    return 6;
+#endif
+	return 7;
+}
+
+static char *
+join_path(const char *a, const char *b)
+{
+	size_t	na;
+	size_t	nb;
+	size_t	pos;
+	int	need_slash;
+	char	*p;
+
+	na = strlen(a);
+	nb = strlen(b);
+	need_slash = (na > 0 && a[na - 1] != '/');
+	p = xmalloc(na + (size_t)need_slash + nb + 1);
+	memcpy(p, a, na);
+	pos = na;
+	if (need_slash)
+	    p[pos++] = '/';
+	memcpy(p + pos, b, nb + 1);
+	return p;
+}
+
+static int
+name_cmp(const void *a, const void *b)
+{
+	const char	*sa;
+	const char	*sb;
+
+	sa = *(const char * const *)a;
+	sb = *(const char * const *)b;
+	return strcmp(sa, sb);
+}
+
+static int
+read_file_bytes(FILE *fp, char **out, size_t *out_len)
+{
+	size_t	cap;
+	size_t	len;
+	size_t	nr;
+	char	*buf;
+
+	cap = 0;
+	len = 0;
+	buf = NULL;
+	for (;;) {
+	    if (len + 4096 > cap) {
+	        size_t nc;
+
+	        nc = (cap == 0) ? 8192 : cap * 2;
+	        while (nc < len + 4096)
+	            nc *= 2;
+	        buf = xrealloc(buf, nc);
+	        cap = nc;
+	    }
+
+	    nr = fread(buf + len, 1, 4096, fp);
+	    len += nr;
+	    if (nr < 4096) {
+	        if (ferror(fp)) {
+	            free(buf);
+	            return 0;
+	        }
+	        break;
+	    }
+	}
+
+	if (buf == NULL)
+	    buf = xmalloc(1);
+	*out = buf;
+	*out_len = len;
+	return 1;
+}
+
+static int
+is_text_bytes(const char *buf, size_t len)
+{
+	size_t	i;
+
+	for (i = 0; i < len; i++) {
+	    if (buf[i] == '\0')
+	        return 0;
+	}
+	return 1;
+}
+
+static void
+file_to_lines(struct file_data *f)
+{
+	struct str_list	ls;
+	size_t		start;
+	size_t		i;
+
+	ls.v = NULL;
+	ls.n = 0;
+	ls.cap = 0;
+
+	start = 0;
+	for (i = 0; i < f->len; i++) {
+	    if (f->bytes[i] == '\n') {
+	        size_t	ln;
+	        char	*s;
+
+	        ln = i - start + 1;
+	        s = xmalloc(ln + 1);
+	        memcpy(s, f->bytes + start, ln);
+	        s[ln] = '\0';
+	        if (ls.n == ls.cap) {
+	            int nc;
+
+	            nc = (ls.cap == 0) ? 32 : ls.cap * 2;
+	            ls.v = xrealloc(ls.v, (size_t)nc * sizeof(*ls.v));
+	            ls.cap = nc;
+	        }
+	        ls.v[ls.n++] = s;
+	        start = i + 1;
+	    }
+	}
+	if (start < f->len) {
+	    size_t	ln2;
+	    char	*s2;
+
+	    ln2 = f->len - start;
+	    s2 = xmalloc(ln2 + 1);
+	    memcpy(s2, f->bytes + start, ln2);
+	    s2[ln2] = '\0';
+	    if (ls.n == ls.cap) {
+	        int nc;
+
+	        nc = (ls.cap == 0) ? 32 : ls.cap * 2;
+	        ls.v = xrealloc(ls.v, (size_t)nc * sizeof(*ls.v));
+	        ls.cap = nc;
+	    }
+	    ls.v[ls.n++] = s2;
+	}
+
+	f->lines = ls.v;
+	f->line_count = ls.n;
+}
+
+static void
+read_operand(const char *path, struct file_data *out)
+{
+	FILE	*fp;
+
+	memset(out, 0, sizeof(*out));
+	out->path = xstrdup(path);
+
+	if (strcmp(path, "-") == 0) {
+	    fp = stdin;
+	    out->has_stat = 0;
+	} else {
+	    if (stat(path, &out->st) != 0)
+	        err(EXIT_ERR, "%s", path);
+	    out->has_stat = 1;
+	    fp = fopen(path, "rb");
+	    if (fp == NULL)
+	        err(EXIT_ERR, "%s", path);
+	}
+
+	if (!read_file_bytes(fp, &out->bytes, &out->len)) {
+	    if (fp != stdin)
+	        fclose(fp);
+	    errx(EXIT_ERR, "%s: read error", path);
+	}
+	if (fp != stdin)
+	    fclose(fp);
+
+	out->is_text = is_text_bytes(out->bytes, out->len);
+	if (out->is_text)
+	    file_to_lines(out);
+}
+
+static void
+free_file_data(struct file_data *f)
+{
+	int	i;
+
+	if (f == NULL)
+	    return;
+	free(f->path);
+	free(f->bytes);
+	if (f->lines != NULL) {
+	    for (i = 0; i < f->line_count; i++)
+	        free(f->lines[i]);
+	}
+	free(f->lines);
+	memset(f, 0, sizeof(*f));
+}
+
+static int
+line_equal_ws(const char *a, const char *b)
+{
+	size_t	ea;
+	size_t	eb;
+	size_t	ia;
+	size_t	ib;
+
+	ia = 0;
+	ib = 0;
+	ea = strlen(a);
+	eb = strlen(b);
+
+	if (ea > 0 && a[ea - 1] == '\n')
+	    ea--;
+	if (eb > 0 && b[eb - 1] == '\n')
+	    eb--;
+	while (ea > 0 && is_space_no_nl((unsigned char)a[ea - 1]))
+	    ea--;
+	while (eb > 0 && is_space_no_nl((unsigned char)b[eb - 1]))
+	    eb--;
+
+	while (ia < ea && ib < eb) {
+	    int wa;
+	    int wb;
+
+	    wa = is_space_no_nl((unsigned char)a[ia]);
+	    wb = is_space_no_nl((unsigned char)b[ib]);
+	    if (wa || wb) {
+	        if (!wa || !wb)
+	            return 0;
+	        while (ia < ea && is_space_no_nl((unsigned char)a[ia]))
+	            ia++;
+	        while (ib < eb && is_space_no_nl((unsigned char)b[ib]))
+	            ib++;
+	        continue;
+	    }
+	    if (a[ia] != b[ib])
+	        return 0;
+	    ia++;
+	    ib++;
+	}
+	return ia == ea && ib == eb;
+}
+
+static int
+line_equal(const char *a, const char *b, const struct options *opt)
+{
+	if (opt->ignore_space)
+	    return line_equal_ws(a, b);
+	return strcmp(a, b) == 0;
+}
+
+static int
+build_file_hunks(const struct file_data *a, const struct file_data *b,
+    const struct options *opt, struct hunk_list *out)
+{
+	size_t	cols;
+	size_t	rows;
+	size_t	sz;
+	int	*dp;
+	int	i;
+	int	j;
+	int	m;
+	int	n;
+
+	out->v = NULL;
+	out->n = 0;
+	out->cap = 0;
+
+	n = a->line_count;
+	m = b->line_count;
+	rows = (size_t)n + 1;
+	cols = (size_t)m + 1;
+	if (rows != 0 && cols > ((size_t)-1) / rows)
+	    return 0;
+	sz = rows * cols;
+	if (sz > ((size_t)-1) / sizeof(*dp))
+	    return 0;
+
+	dp = calloc(sz, sizeof(*dp));
+	if (dp == NULL)
+	    return 0;
+
+	for (i = n - 1; i >= 0; i--) {
+	    for (j = m - 1; j >= 0; j--) {
+	        size_t idx;
+
+	        idx = (size_t)i * cols + (size_t)j;
+	        if (line_equal(a->lines[i], b->lines[j], opt)) {
+	            dp[idx] = dp[(size_t)(i + 1) * cols +
+	                (size_t)(j + 1)] + 1;
+	        } else {
+	            int d;
+	            int r;
+
+	            d = dp[(size_t)(i + 1) * cols + (size_t)j];
+	            r = dp[(size_t)i * cols + (size_t)(j + 1)];
+	            dp[idx] = d >= r ? d : r;
+	        }
+	    }
+	}
+
+	i = 0;
+	j = 0;
+	while (i < n || j < m) {
+	    int ei;
+	    int ej;
+	    int si;
+	    int sj;
+
+	    if (i < n && j < m && line_equal(a->lines[i], b->lines[j], opt)) {
+	        i++;
+	        j++;
+	        continue;
+	    }
+
+	    si = i;
+	    sj = j;
+	    while (i < n || j < m) {
+	        if (i < n && j < m &&
+	            line_equal(a->lines[i], b->lines[j], opt))
+	            break;
+	        if (j >= m || (i < n &&
+	            dp[(size_t)(i + 1) * cols + (size_t)j] >=
+	            dp[(size_t)i * cols + (size_t)(j + 1)]))
+	            i++;
+	        else
+	            j++;
+	    }
+	    ei = i;
+	    ej = j;
+	    hunk_push(out, si, ei, sj, ej);
+	}
+
+	free(dp);
+	return 1;
+}
+
+static void
+hunk_push(struct hunk_list *hs, int a0, int a1, int b0, int b1)
+{
+	if (hs->n == hs->cap) {
+	    int nc;
+
+	    nc = (hs->cap == 0) ? 32 : hs->cap * 2;
+	    hs->v = xrealloc(hs->v, (size_t)nc * sizeof(*hs->v));
+	    hs->cap = nc;
+	}
+	hs->v[hs->n].a0 = a0;
+	hs->v[hs->n].a1 = a1;
+	hs->v[hs->n].b0 = b0;
+	hs->v[hs->n].b1 = b1;
+	hs->n++;
+}
+
+static void
+print_line_prefixed(const char *pfx, const char *line)
+{
+	size_t	n;
+
+	n = strlen(line);
+	fputs(pfx, stdout);
+	fputs(line, stdout);
+	if (n == 0 || line[n - 1] != '\n')
+	    fputc('\n', stdout);
+}
+
+static void
+print_default_range(int s0, int e0)
+{
+	int	a;
+	int	b;
+
+	a = s0 + 1;
+	b = e0;
+	if (a == b)
+	    printf("%d", a);
+	else
+	    printf("%d,%d", a, b);
+}
+
+static void
+emit_default(const struct file_data *a, const struct file_data *b,
+    const struct hunk_list *hs)
+{
+	const struct hunk	*x;
+	int			h;
+	int			k;
+
+	for (h = 0; h < hs->n; h++) {
+	    x = &hs->v[h];
+	    if (x->a0 == x->a1) {
+	        printf("%da", x->a0);
+	        print_default_range(x->b0, x->b1);
+	        printf("\n");
+	        for (k = x->b0; k < x->b1; k++)
+	            print_line_prefixed("> ", b->lines[k]);
+	    } else if (x->b0 == x->b1) {
+	        print_default_range(x->a0, x->a1);
+	        printf("d%d\n", x->b0);
+	        for (k = x->a0; k < x->a1; k++)
+	            print_line_prefixed("< ", a->lines[k]);
+	    } else {
+	        print_default_range(x->a0, x->a1);
+	        printf("c");
+	        print_default_range(x->b0, x->b1);
+	        printf("\n");
+	        for (k = x->a0; k < x->a1; k++)
+	            print_line_prefixed("< ", a->lines[k]);
+	        printf("---\n");
+	        for (k = x->b0; k < x->b1; k++)
+	            print_line_prefixed("> ", b->lines[k]);
+	    }
+	}
+}
+
+static void
+print_ed_payload(const struct file_data *b, int s, int e)
+{
+	int		k;
+	const char	*ln;
+	size_t		n;
+
+	for (k = s; k < e; k++) {
+	    ln = b->lines[k];
+	    if (strcmp(ln, ".") == 0 || strcmp(ln, ".\n") == 0)
+	        printf("..\n");
+	    else {
+	        fputs(ln, stdout);
+	        n = strlen(ln);
+	        if (n == 0 || ln[n - 1] != '\n')
+	            fputc('\n', stdout);
+	    }
+	}
+	printf(".\n");
+}
+
+static void
+emit_ed(const struct file_data *a, const struct file_data *b,
+    const struct hunk_list *hs)
+{
+	const struct hunk	*x;
+	int			alen;
+	int			blen;
+	int			h;
+
+	(void)a;
+	for (h = hs->n - 1; h >= 0; h--) {
+	    x = &hs->v[h];
+	    alen = x->a1 - x->a0;
+	    blen = x->b1 - x->b0;
+	    if (alen == 0) {
+	        printf("%da\n", x->a0);
+	        print_ed_payload(b, x->b0, x->b1);
+	    } else if (blen == 0) {
+	        if (alen == 1)
+	            printf("%dd\n", x->a0 + 1);
+	        else
+	            printf("%d,%dd\n", x->a0 + 1, x->a1);
+	    } else {
+	        if (alen == 1)
+	            printf("%dc\n", x->a0 + 1);
+	        else
+	            printf("%d,%dc\n", x->a0 + 1, x->a1);
+	        print_ed_payload(b, x->b0, x->b1);
+	    }
+	}
+}
+
+static void
+print_fwd_addr(int s, int e)
+{
+	if (e - s <= 1)
+	    printf("%d", s + 1);
+	else
+	    printf("%d %d", s + 1, e);
+}
+
+static void
+emit_forward(const struct file_data *a, const struct file_data *b,
+    const struct hunk_list *hs)
+{
+	const struct hunk	*x;
+	int			alen;
+	int			blen;
+	int			h;
+
+	(void)a;
+	for (h = 0; h < hs->n; h++) {
+	    x = &hs->v[h];
+	    alen = x->a1 - x->a0;
+	    blen = x->b1 - x->b0;
+	    if (alen == 0) {
+	        printf("a %d\n", x->a0);
+	        print_ed_payload(b, x->b0, x->b1);
+	    } else if (blen == 0) {
+	        printf("d ");
+	        print_fwd_addr(x->a0, x->a1);
+	        printf("\n");
+	    } else {
+	        printf("c ");
+	        print_fwd_addr(x->a0, x->a1);
+	        printf("\n");
+	        print_ed_payload(b, x->b0, x->b1);
+	    }
+	}
+}
+
+static void
+fmt_context_time(const struct file_data *f, char *buf, size_t buflen)
+{
+	char		day[16];
+	char		hmy[32];
+	char		mon[16];
+	time_t		t;
+	struct tm	*tmv;
+
+	t = f->has_stat ? f->st.st_mtime : time(NULL);
+	tmv = localtime(&t);
+	if (tmv == NULL) {
+	    strncpy(buf, "??? ??? ?? ??:??:?? ????", buflen);
+	    buf[buflen - 1] = '\0';
+	    return;
+	}
+	if (strftime(day, sizeof(day), "%a", tmv) == 0 ||
+	    strftime(mon, sizeof(mon), "%b", tmv) == 0 ||
+	    strftime(hmy, sizeof(hmy), "%H:%M:%S %Y", tmv) == 0) {
+	    strncpy(buf, "??? ??? ?? ??:??:?? ????", buflen);
+	    buf[buflen - 1] = '\0';
+	    return;
+	}
+	snprintf(buf, buflen, "%s %s %2d %s", day, mon, tmv->tm_mday, hmy);
+}
+
+static void
+fmt_unified_time(const struct file_data *f, char *buf, size_t buflen)
+{
+	char		base[64];
+	char		sign;
+	long		off;
+	long		off_abs;
+	long		off_hh;
+	long		off_mm;
+	time_t		g_as_local;
+	time_t		t;
+	struct tm	gtm;
+	struct tm	*gtmp;
+	struct tm	*tmv;
+
+	t = f->has_stat ? f->st.st_mtime : time(NULL);
+	tmv = localtime(&t);
+	if (tmv == NULL) {
+	    strncpy(buf, "1970-01-01 00:00:00.000000000 +0000", buflen);
+	    buf[buflen - 1] = '\0';
+	    return;
+	}
+	if (strftime(base, sizeof(base), "%Y-%m-%d %H:%M:%S", tmv) == 0)
+	    strcpy(base, "1970-01-01 00:00:00");
+	gtmp = gmtime(&t);
+	if (gtmp == NULL) {
+	    snprintf(buf, buflen, "%s.000000000 +0000", base);
+	    return;
+	}
+	gtm = *gtmp;
+	g_as_local = mktime(&gtm);
+	if (g_as_local == (time_t)-1) {
+	    snprintf(buf, buflen, "%s.000000000 +0000", base);
+	    return;
+	}
+	off = (long)difftime(t, g_as_local);
+	sign = off < 0 ? '-' : '+';
+	off_abs = off < 0 ? -off : off;
+	off_hh = off_abs / 3600;
+	off_mm = (off_abs % 3600) / 60;
+	snprintf(buf, buflen, "%s.000000000 %c%02ld%02ld", base, sign, off_hh,
+	    off_mm);
+}
+
+static void
+print_context_old_range(int s, int e)
+{
+	int len;
+
+	len = e - s;
+	if (len <= 0)
+	    printf("*** %d ****\n", s);
+	else if (len == 1)
+	    printf("*** %d ****\n", s + 1);
+	else
+	    printf("*** %d,%d ****\n", s + 1, e);
+}
+
+static void
+print_context_new_range(int s, int e)
+{
+	int len;
+
+	len = e - s;
+	if (len <= 0)
+	    printf("--- %d ----\n", s);
+	else if (len == 1)
+	    printf("--- %d ----\n", s + 1);
+	else
+	    printf("--- %d,%d ----\n", s + 1, e);
+}
+
+static void
+emit_context(const struct file_data *a, const struct file_data *b,
+    const struct hunk_list *hs, int ctx)
+{
+	char			*mark_a;
+	char			*mark_b;
+	char			ta[64];
+	char			tb[64];
+	const struct hunk	*x;
+	int			ge;
+	int			gi;
+	int			gs;
+	int			h;
+	int			i;
+	int			new_e;
+	int			new_s;
+	int			old_e;
+	int			old_s;
+	int			size_a;
+	int			size_b;
+
+	if (hs->n == 0)
+	    return;
+
+	fmt_context_time(a, ta, sizeof(ta));
+	fmt_context_time(b, tb, sizeof(tb));
+	printf("*** %s %s\n", a->path, ta);
+	printf("--- %s %s\n", b->path, tb);
+
+	size_a = a->line_count > 0 ? a->line_count : 1;
+	size_b = b->line_count > 0 ? b->line_count : 1;
+	gs = 0;
+	while (gs < hs->n) {
+	    ge = gs;
+	    while (ge + 1 < hs->n) {
+	        const struct hunk *cur;
+	        const struct hunk *nxt;
+
+	        cur = &hs->v[ge];
+	        nxt = &hs->v[ge + 1];
+	        if ((nxt->a0 - cur->a1) <= 2 * ctx ||
+	            (nxt->b0 - cur->b1) <= 2 * ctx)
+	            ge++;
+	        else
+	            break;
+	    }
+
+	    old_s = hs->v[gs].a0 - ctx;
+	    if (old_s < 0)
+	        old_s = 0;
+	    old_e = hs->v[ge].a1 + ctx;
+	    if (old_e > a->line_count)
+	        old_e = a->line_count;
+	    new_s = hs->v[gs].b0 - ctx;
+	    if (new_s < 0)
+	        new_s = 0;
+	    new_e = hs->v[ge].b1 + ctx;
+	    if (new_e > b->line_count)
+	        new_e = b->line_count;
+
+	    mark_a = xmalloc((size_t)size_a);
+	    mark_b = xmalloc((size_t)size_b);
+	    memset(mark_a, ' ', (size_t)size_a);
+	    memset(mark_b, ' ', (size_t)size_b);
+
+	    for (h = gs; h <= ge; h++) {
+	        x = &hs->v[h];
+	        if (x->a1 > x->a0 && x->b1 > x->b0) {
+	            for (i = x->a0; i < x->a1; i++)
+	                mark_a[i] = '!';
+	            for (i = x->b0; i < x->b1; i++)
+	                mark_b[i] = '!';
+	        } else if (x->a1 > x->a0) {
+	            for (i = x->a0; i < x->a1; i++)
+	                mark_a[i] = '-';
+	        } else {
+	            for (i = x->b0; i < x->b1; i++)
+	                mark_b[i] = '+';
+	        }
+	    }
+
+	    printf("***************\n");
+	    print_context_old_range(old_s, old_e);
+	    for (gi = old_s; gi < old_e; gi++) {
+	        char pfx[3];
+
+	        pfx[0] = mark_a[gi] == '-' ? '-' :
+	            (mark_a[gi] == '!' ? '!' : ' ');
+	        pfx[1] = ' ';
+	        pfx[2] = '\0';
+	        print_line_prefixed(pfx, a->lines[gi]);
+	    }
+	    print_context_new_range(new_s, new_e);
+	    for (gi = new_s; gi < new_e; gi++) {
+	        char pfx[3];
+
+	        pfx[0] = mark_b[gi] == '+' ? '+' :
+	            (mark_b[gi] == '!' ? '!' : ' ');
+	        pfx[1] = ' ';
+	        pfx[2] = '\0';
+	        print_line_prefixed(pfx, b->lines[gi]);
+	    }
+
+	    free(mark_a);
+	    free(mark_b);
+	    gs = ge + 1;
+	}
+}
+
+static void
+print_unified_range_part(int s, int e)
+{
+	int	len;
+	int	pos;
+
+	len = e - s;
+	pos = len == 0 ? s : s + 1;
+	if (len == 1)
+	    printf("%d", pos);
+	else
+	    printf("%d,%d", pos, len);
+}
+
+static void
+emit_unified(const struct file_data *a, const struct file_data *b,
+    const struct hunk_list *hs, int ctx)
+{
+	char			ta[96];
+	char			tb[96];
+	const struct hunk	*x;
+	int			cur_a;
+	int			cur_b;
+	int			ge;
+	int			gs;
+	int			h;
+	int			i;
+	int			new_e;
+	int			new_s;
+	int			old_e;
+	int			old_s;
+
+	if (hs->n == 0)
+	    return;
+
+	fmt_unified_time(a, ta, sizeof(ta));
+	fmt_unified_time(b, tb, sizeof(tb));
+	printf("--- %s\t%s\n", a->path, ta);
+	printf("+++ %s\t%s\n", b->path, tb);
+
+	gs = 0;
+	while (gs < hs->n) {
+	    ge = gs;
+	    while (ge + 1 < hs->n) {
+	        const struct hunk *cur;
+	        const struct hunk *nxt;
+
+	        cur = &hs->v[ge];
+	        nxt = &hs->v[ge + 1];
+	        if ((nxt->a0 - cur->a1) <= 2 * ctx ||
+	            (nxt->b0 - cur->b1) <= 2 * ctx)
+	            ge++;
+	        else
+	            break;
+	    }
+
+	    old_s = hs->v[gs].a0 - ctx;
+	    if (old_s < 0)
+	        old_s = 0;
+	    old_e = hs->v[ge].a1 + ctx;
+	    if (old_e > a->line_count)
+	        old_e = a->line_count;
+	    new_s = hs->v[gs].b0 - ctx;
+	    if (new_s < 0)
+	        new_s = 0;
+	    new_e = hs->v[ge].b1 + ctx;
+	    if (new_e > b->line_count)
+	        new_e = b->line_count;
+
+	    printf("@@ -");
+	    print_unified_range_part(old_s, old_e);
+	    printf(" +");
+	    print_unified_range_part(new_s, new_e);
+	    printf(" @@\n");
+
+	    cur_a = old_s;
+	    cur_b = new_s;
+	    for (h = gs; h <= ge; h++) {
+	        x = &hs->v[h];
+	        while (cur_a < x->a0 && cur_b < x->b0) {
+	            print_line_prefixed(" ", a->lines[cur_a]);
+	            cur_a++;
+	            cur_b++;
+	        }
+	        for (i = cur_a; i < x->a1; i++)
+	            print_line_prefixed("-", a->lines[i]);
+	        for (i = cur_b; i < x->b1; i++)
+	            print_line_prefixed("+", b->lines[i]);
+	        cur_a = x->a1;
+	        cur_b = x->b1;
+	    }
+	    while (cur_a < old_e && cur_b < new_e) {
+	        print_line_prefixed(" ", a->lines[cur_a]);
+	        cur_a++;
+	        cur_b++;
+	    }
+
+	    gs = ge + 1;
+	}
+}
+
+static void
+make_diffopt_string(const struct options *opt, char *buf, size_t buflen)
+{
+	char	fmtbuf[32];
+	char	flags[16];
+
+	fmtbuf[0] = '\0';
+	flags[0] = '\0';
+	if (opt->fmt == FMT_CONTEXT) {
+	    if (opt->context == 3)
+	        strcpy(fmtbuf, "-c");
+	    else
+	        snprintf(fmtbuf, sizeof(fmtbuf), "-C %d", opt->context);
+	} else if (opt->fmt == FMT_UNIFIED) {
+	    if (opt->context == 3)
+	        strcpy(fmtbuf, "-u");
+	    else
+	        snprintf(fmtbuf, sizeof(fmtbuf), "-U %d", opt->context);
+	} else if (opt->fmt == FMT_ED)
+	    strcpy(fmtbuf, "-e");
+	else if (opt->fmt == FMT_FORWARD)
+	    strcpy(fmtbuf, "-f");
+
+	if (opt->ignore_space)
+	    strcat(flags, " -b");
+	if (opt->recursive)
+	    strcat(flags, " -r");
+
+	if (fmtbuf[0] != '\0')
+	    snprintf(buf, buflen, "%s%s", fmtbuf, flags);
+	else if (flags[0] != '\0')
+	    snprintf(buf, buflen, "%s", flags + 1);
+	else
+	    buf[0] = '\0';
+}
+
+static int
+compare_text_files(const struct file_data *a, const struct file_data *b,
+    const struct options *opt, int emit)
+{
+	struct hunk_list	hs;
+
+	if (!build_file_hunks(a, b, opt, &hs))
+	    errx(EXIT_ERR, "out of memory");
+	if (hs.n == 0) {
+	    free(hs.v);
+	    return EXIT_SAME;
+	}
+
+	if (emit) {
+	    switch (opt->fmt) {
+	    case FMT_DEFAULT:
+	        emit_default(a, b, &hs);
+	        break;
+	    case FMT_CONTEXT:
+	        emit_context(a, b, &hs, opt->context);
+	        break;
+	    case FMT_UNIFIED:
+	        emit_unified(a, b, &hs, opt->context);
+	        break;
+	    case FMT_ED:
+	        emit_ed(a, b, &hs);
+	        break;
+	    case FMT_FORWARD:
+	        emit_forward(a, b, &hs);
+	        break;
+	    }
+	}
+
+	free(hs.v);
+	return EXIT_DIFF;
+}
+
+static int
+compare_file_paths(const char *path1, const char *path2, const char *label1,
+    const char *label2, const struct options *opt, int emit)
+{
+	struct file_data	a;
+	struct file_data	b;
+	int			rc;
+
+	if (strcmp(path1, "-") == 0 && strcmp(path2, "-") == 0)
+	    errx(EXIT_ERR, "both operands cannot be '-'");
+
+	read_operand(path1, &a);
+	read_operand(path2, &b);
+	free(a.path);
+	free(b.path);
+	a.path = xstrdup(label1);
+	b.path = xstrdup(label2);
+
+	if (!a.is_text || !b.is_text) {
+	    if (a.len != b.len || memcmp(a.bytes, b.bytes, a.len) != 0) {
+	        if (emit)
+	            printf("Binary files %s and %s differ\n", label1, label2);
+	        rc = EXIT_DIFF;
+	    } else
+	        rc = EXIT_SAME;
+	} else
+	    rc = compare_text_files(&a, &b, opt, emit);
+
+	free_file_data(&a);
+	free_file_data(&b);
+	return rc;
+}
+
+static int
+inode_stack_contains(const struct inode_stack *st, dev_t dev, ino_t ino)
+{
+	int	i;
+
+	for (i = 0; i < st->n; i++) {
+	    if (st->v[i].dev == dev && st->v[i].ino == ino)
+	        return 1;
+	}
+	return 0;
+}
+
+static void
+inode_stack_push(struct inode_stack *st, dev_t dev, ino_t ino)
+{
+	if (st->n == st->cap) {
+	    int nc;
+
+	    nc = (st->cap == 0) ? 32 : st->cap * 2;
+	    st->v = xrealloc(st->v, (size_t)nc * sizeof(*st->v));
+	    st->cap = nc;
+	}
+	st->v[st->n].dev = dev;
+	st->v[st->n].ino = ino;
+	st->n++;
+}
+
+static void
+inode_stack_pop(struct inode_stack *st)
+{
+	if (st->n > 0)
+	    st->n--;
+}
+
+static int
+read_dir_names(const char *path, struct str_list *out)
+{
+	DIR		*d;
+	struct dirent	*de;
+
+	out->v = NULL;
+	out->n = 0;
+	out->cap = 0;
+
+	d = opendir(path);
+	if (d == NULL) {
+	    warn("%s", path);
+	    return EXIT_ERR;
+	}
+	for (;;) {
+	    de = readdir(d);
+	    if (de == NULL)
+	        break;
+	    if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
+	        continue;
+	    if (out->n == out->cap) {
+	        int nc;
+
+	        nc = (out->cap == 0) ? 32 : out->cap * 2;
+	        out->v = xrealloc(out->v, (size_t)nc * sizeof(*out->v));
+	        out->cap = nc;
+	    }
+	    out->v[out->n++] = xstrdup(de->d_name);
+	}
+	if (closedir(d) != 0) {
+	    warn("%s", path);
+	    return EXIT_ERR;
+	}
+	qsort(out->v, (size_t)out->n, sizeof(*out->v), name_cmp);
+	return EXIT_SAME;
+}
+
+static void
+free_str_list(struct str_list *ls)
+{
+	int i;
+
+	for (i = 0; i < ls->n; i++)
+	    free(ls->v[i]);
+	free(ls->v);
+	ls->v = NULL;
+	ls->n = 0;
+	ls->cap = 0;
+}
+
+static int
+compare_dirs(const char *dir1, const char *dir2, const char *disp1,
+    const char *disp2, const struct options *opt, struct inode_stack *stack)
+{
+	struct str_list	a;
+	struct str_list	b;
+	char		optbuf[128];
+	int		i;
+	int		j;
+	int		rc;
+	int		saw_diff;
+
+	rc = read_dir_names(dir1, &a);
+	if (rc == EXIT_ERR)
+	    return EXIT_ERR;
+	rc = read_dir_names(dir2, &b);
+	if (rc == EXIT_ERR) {
+	    free_str_list(&a);
+	    return EXIT_ERR;
+	}
+
+	saw_diff = 0;
+	make_diffopt_string(opt, optbuf, sizeof(optbuf));
+
+	i = 0;
+	j = 0;
+	while (i < a.n || j < b.n) {
+	    int c;
+
+	    if (i < a.n && j < b.n)
+	        c = strcmp(a.v[i], b.v[j]);
+	    else
+	        c = (i < a.n) ? -1 : 1;
+
+	    if (c < 0) {
+	        printf("Only in %s: %s\n", disp1, a.v[i]);
+	        saw_diff = 1;
+	        i++;
+	        continue;
+	    }
+	    if (c > 0) {
+	        printf("Only in %s: %s\n", disp2, b.v[j]);
+	        saw_diff = 1;
+	        j++;
+	        continue;
+	    }
+
+	    {
+	        char		*p1;
+	        char		*p2;
+	        char		*d1;
+	        char		*d2;
+	        int		rr;
+	        struct stat	s1;
+	        struct stat	s2;
+
+	        p1 = join_path(dir1, a.v[i]);
+	        p2 = join_path(dir2, b.v[j]);
+	        d1 = join_path(disp1, a.v[i]);
+	        d2 = join_path(disp2, b.v[j]);
+	        if (lstat(p1, &s1) != 0 || lstat(p2, &s2) != 0) {
+	            warn("%s or %s", p1, p2);
+	            free(p1);
+	            free(p2);
+	            free(d1);
+	            free(d2);
+	            free_str_list(&a);
+	            free_str_list(&b);
+	            return EXIT_ERR;
+	        }
+
+	        if (S_ISDIR(s1.st_mode) && S_ISDIR(s2.st_mode)) {
+	            if (!opt->recursive)
+	                printf("Common subdirectories: %s and %s\n",
+	                    d1, d2);
+	            else if (inode_stack_contains(stack, s1.st_dev,
+	                s1.st_ino) || inode_stack_contains(stack,
+	                s2.st_dev, s2.st_ino)) {
+	                warnx("infinite directory loop detected at %s and "
+	                    "%s", d1, d2);
+	            } else {
+	                inode_stack_push(stack, s1.st_dev, s1.st_ino);
+	                inode_stack_push(stack, s2.st_dev, s2.st_ino);
+	                rr = compare_dirs(p1, p2, d1, d2, opt, stack);
+	                inode_stack_pop(stack);
+	                inode_stack_pop(stack);
+	                if (rr == EXIT_ERR) {
+	                    free(p1);
+	                    free(p2);
+	                    free(d1);
+	                    free(d2);
+	                    free_str_list(&a);
+	                    free_str_list(&b);
+	                    return EXIT_ERR;
+	                }
+	                if (rr == EXIT_DIFF)
+	                    saw_diff = 1;
+	            }
+	        } else if (S_ISREG(s1.st_mode) && S_ISREG(s2.st_mode)) {
+	            rr = compare_file_paths(p1, p2, d1, d2, opt, 0);
+	            if (rr == EXIT_ERR) {
+	                free(p1);
+	                free(p2);
+	                free(d1);
+	                free(d2);
+	                free_str_list(&a);
+	                free_str_list(&b);
+	                return EXIT_ERR;
+	            }
+	            if (rr == EXIT_DIFF) {
+	                if (optbuf[0] != '\0')
+	                    printf("diff %s %s %s\n", optbuf, d1,
+	                        d2);
+	                else
+	                    printf("diff %s %s\n", d1, d2);
+	                rr = compare_file_paths(p1, p2, d1, d2, opt, 1);
+	                if (rr == EXIT_ERR) {
+	                    free(p1);
+	                    free(p2);
+	                    free(d1);
+	                    free(d2);
+	                    free_str_list(&a);
+	                    free_str_list(&b);
+	                    return EXIT_ERR;
+	                }
+	                saw_diff = 1;
+	            }
+	        } else {
+	            int t1;
+	            int t2;
+
+	            t1 = ftype_rank(s1.st_mode);
+	            t2 = ftype_rank(s2.st_mode);
+	            if (t1 <= t2)
+	                printf("File %s is a %s while file %s is a %s\n",
+	                    d1, ftype_name(s1.st_mode), d2,
+	                    ftype_name(s2.st_mode));
+	            else
+	                printf("File %s is a %s while file %s is a %s\n",
+	                    d2, ftype_name(s2.st_mode), d1,
+	                    ftype_name(s1.st_mode));
+	            saw_diff = 1;
+	        }
+
+	        free(p1);
+	        free(p2);
+	        free(d1);
+	        free(d2);
+	    }
+
+	    i++;
+	    j++;
+	}
+
+	free_str_list(&a);
+	free_str_list(&b);
+	return saw_diff ? EXIT_DIFF : EXIT_SAME;
+}
+
+static int
+is_dir_path(const char *p, struct stat *st)
+{
+	if (strcmp(p, "-") == 0)
+	    return 0;
+	if (stat(p, st) != 0)
+	    return 0;
+	return S_ISDIR(st->st_mode);
+}
+
+static int
+parse_options(int argc, char **argv, struct options *opt, int *argi)
+{
+	int	ch;
+	int	v;
+
+	opt->fmt = FMT_DEFAULT;
+	opt->context = 3;
+	opt->ignore_space = 0;
+	opt->recursive = 0;
+
+	optind = 1;
+	opterr = 0;
+	while ((ch = getopt(argc, argv, "bC:cefU:ur")) != -1) {
+	    switch (ch) {
+	    case 'C':
+	        if (!parse_nonneg(optarg, &v) || v <= 0)
+	            errx(EXIT_ERR, "-C requires positive decimal integer");
+	        opt->fmt = FMT_CONTEXT;
+	        opt->context = v;
+	        break;
+	    case 'U':
+	        if (!parse_nonneg(optarg, &v))
+	            errx(EXIT_ERR, "-U requires decimal integer");
+	        opt->fmt = FMT_UNIFIED;
+	        opt->context = v;
+	        break;
+	    case 'b':
+	        opt->ignore_space = 1;
+	        break;
+	    case 'c':
+	        opt->fmt = FMT_CONTEXT;
+	        opt->context = 3;
+	        break;
+	    case 'e':
+	        opt->fmt = FMT_ED;
+	        break;
+	    case 'f':
+	        opt->fmt = FMT_FORWARD;
+	        break;
+	    case 'r':
+	        opt->recursive = 1;
+	        break;
+	    case 'u':
+	        opt->fmt = FMT_UNIFIED;
+	        opt->context = 3;
+	        break;
+	    default:
+	        usage();
+	    }
+	}
+
+	*argi = optind;
+	if (argc - optind != 2)
+	    usage();
+	return EXIT_SAME;
+}
+
+int
+main(int argc, char **argv)
+{
+	const char		*base;
+	char			*in_dir;
+	char			*op1;
+	char			*op2;
+	struct inode_stack	stack;
+	struct options		opt;
+	struct stat		st1;
+	struct stat		st2;
+	int			argi;
+	int			d1;
+	int			d2;
+	int			rc;
+
+	if (argc > 0 && argv[0] != NULL) {
+	    base = strrchr(argv[0], '/');
+	    if (base != NULL)
+	        progname = base + 1;
+	    else
+	        progname = argv[0];
+	}
+
+	parse_options(argc, argv, &opt, &argi);
+	op1 = argv[argi];
+	op2 = argv[argi + 1];
+
+	d1 = is_dir_path(op1, &st1);
+	d2 = is_dir_path(op2, &st2);
+
+	if (d1 && d2) {
+	    stack.v = NULL;
+	    stack.n = 0;
+	    stack.cap = 0;
+	    inode_stack_push(&stack, st1.st_dev, st1.st_ino);
+	    inode_stack_push(&stack, st2.st_dev, st2.st_ino);
+	    rc = compare_dirs(op1, op2, op1, op2, &opt, &stack);
+	    free(stack.v);
+	    return rc;
+	}
+
+	if (d1 != d2) {
+	    char *dir;
+	    char *file;
+
+	    if (d1) {
+	        dir = op1;
+	        file = op2;
+	    } else {
+	        dir = op2;
+	        file = op1;
+	    }
+	    base = strrchr(file, '/');
+	    if (base == NULL)
+	        base = file;
+	    else
+	        base++;
+
+	    in_dir = join_path(dir, base);
+	    if (d1)
+	        rc = compare_file_paths(in_dir, file, in_dir, file, &opt, 1);
+	    else
+	        rc = compare_file_paths(file, in_dir, file, in_dir, &opt, 1);
+	    free(in_dir);
+	    return rc;
+	}
+
+	return compare_file_paths(op1, op2, op1, op2, &opt, 1);
+}