68e6c8
diff -ur b/attr.c c/attr.c
68e6c8
--- b/attr.c	2013-06-10 22:01:55.000000000 +0200
68e6c8
+++ c/attr.c	2023-02-21 12:25:20.735892607 +0100
68e6c8
@@ -12,6 +12,7 @@
68e6c8
 #include "exec_cmd.h"
68e6c8
 #include "attr.h"
68e6c8
 #include "dir.h"
68e6c8
+#include "utf8.h"
68e6c8
 
68e6c8
 const char git_attr__true[] = "(builtin)true";
68e6c8
 const char git_attr__false[] = "\0(builtin)false";
68e6c8
@@ -55,26 +56,36 @@
68e6c8
 	return val;
68e6c8
 }
68e6c8
 
68e6c8
-static int invalid_attr_name(const char *name, int namelen)
68e6c8
+static int attr_name_valid(const char *name, size_t namelen)
68e6c8
 {
68e6c8
 	/*
68e6c8
 	 * Attribute name cannot begin with '-' and must consist of
68e6c8
 	 * characters from [-A-Za-z0-9_.].
68e6c8
 	 */
68e6c8
 	if (namelen <= 0 || *name == '-')
68e6c8
-		return -1;
68e6c8
+		return 0;
68e6c8
 	while (namelen--) {
68e6c8
 		char ch = *name++;
68e6c8
 		if (! (ch == '-' || ch == '.' || ch == '_' ||
68e6c8
 		       ('0' <= ch && ch <= '9') ||
68e6c8
 		       ('a' <= ch && ch <= 'z') ||
68e6c8
 		       ('A' <= ch && ch <= 'Z')) )
68e6c8
-			return -1;
68e6c8
+			return 0;
68e6c8
 	}
68e6c8
-	return 0;
68e6c8
+	return 1;
68e6c8
 }
68e6c8
 
68e6c8
-static struct git_attr *git_attr_internal(const char *name, int len)
68e6c8
+static void report_invalid_attr(const char *name, size_t len,
68e6c8
+				const char *src, int lineno)
68e6c8
+{
68e6c8
+	struct strbuf err = STRBUF_INIT;
68e6c8
+	strbuf_addf(&err, _("%.*s is not a valid attribute name"),
68e6c8
+		    (int) len, name);
68e6c8
+	fprintf(stderr, "%s: %s:%d\n", err.buf, src, lineno);
68e6c8
+	strbuf_release(&err;;
68e6c8
+}
68e6c8
+
68e6c8
+static struct git_attr *git_attr_internal(const char *name, size_t len)
68e6c8
 {
68e6c8
 	unsigned hval = hash_name(name, len);
68e6c8
 	unsigned pos = hval % HASHSIZE;
68e6c8
@@ -86,7 +97,7 @@
68e6c8
 			return a;
68e6c8
 	}
68e6c8
 
68e6c8
-	if (invalid_attr_name(name, len))
68e6c8
+	if (!attr_name_valid(name, len))
68e6c8
 		return NULL;
68e6c8
 
68e6c8
 	a = xmalloc(sizeof(*a) + len + 1);
68e6c8
@@ -142,7 +153,7 @@
68e6c8
 		struct git_attr *attr;
68e6c8
 	} u;
68e6c8
 	char is_macro;
68e6c8
-	unsigned num_attr;
68e6c8
+	size_t num_attr;
68e6c8
 	struct attr_state state[FLEX_ARRAY];
68e6c8
 };
68e6c8
 
68e6c8
@@ -159,7 +170,7 @@
68e6c8
 			      struct attr_state *e)
68e6c8
 {
68e6c8
 	const char *ep, *equals;
68e6c8
-	int len;
68e6c8
+	size_t len;
68e6c8
 
68e6c8
 	ep = cp + strcspn(cp, blank);
68e6c8
 	equals = strchr(cp, '=');
68e6c8
@@ -174,10 +185,8 @@
68e6c8
 			cp++;
68e6c8
 			len--;
68e6c8
 		}
68e6c8
-		if (invalid_attr_name(cp, len)) {
68e6c8
-			fprintf(stderr,
68e6c8
-				"%.*s is not a valid attribute name: %s:%d\n",
68e6c8
-				len, cp, src, lineno);
68e6c8
+		if (!attr_name_valid(cp, len)) {
68e6c8
+			report_invalid_attr(cp, len, src, lineno);
68e6c8
 			return NULL;
68e6c8
 		}
68e6c8
 	} else {
68e6c8
@@ -199,8 +208,7 @@
68e6c8
 static struct match_attr *parse_attr_line(const char *line, const char *src,
68e6c8
 					  int lineno, int macro_ok)
68e6c8
 {
68e6c8
-	int namelen;
68e6c8
-	int num_attr, i;
68e6c8
+	size_t namelen, num_attr, i;
68e6c8
 	const char *cp, *name, *states;
68e6c8
 	struct match_attr *res = NULL;
68e6c8
 	int is_macro;
68e6c8
@@ -209,6 +217,12 @@
68e6c8
 	if (!*cp || *cp == '#')
68e6c8
 		return NULL;
68e6c8
 	name = cp;
68e6c8
+
68e6c8
+	if (strlen(line) >= ATTR_MAX_LINE_LENGTH) {
68e6c8
+		warning(_("ignoring overly long attributes line %d"), lineno);
68e6c8
+		return NULL;
68e6c8
+	}
68e6c8
+
68e6c8
 	namelen = strcspn(name, blank);
68e6c8
 	if (strlen(ATTRIBUTE_MACRO_PREFIX) < namelen &&
68e6c8
 	    !prefixcmp(name, ATTRIBUTE_MACRO_PREFIX)) {
68e6c8
@@ -221,10 +235,8 @@
68e6c8
 		name += strlen(ATTRIBUTE_MACRO_PREFIX);
68e6c8
 		name += strspn(name, blank);
68e6c8
 		namelen = strcspn(name, blank);
68e6c8
-		if (invalid_attr_name(name, namelen)) {
68e6c8
-			fprintf(stderr,
68e6c8
-				"%.*s is not a valid attribute name: %s:%d\n",
68e6c8
-				namelen, name, src, lineno);
68e6c8
+		if (!attr_name_valid(name, namelen)) {
68e6c8
+			report_invalid_attr(cp, namelen, src, lineno);
68e6c8
 			return NULL;
68e6c8
 		}
68e6c8
 	}
68e6c8
@@ -241,10 +253,9 @@
68e6c8
 			return NULL;
68e6c8
 	}
68e6c8
 
68e6c8
-	res = xcalloc(1,
68e6c8
-		      sizeof(*res) +
68e6c8
-		      sizeof(struct attr_state) * num_attr +
68e6c8
-		      (is_macro ? 0 : namelen + 1));
68e6c8
+	res = xcalloc(1, st_add3(sizeof(*res),
68e6c8
+				 st_mult(sizeof(struct attr_state), num_attr),
68e6c8
+				 is_macro ? 0 : namelen + 1));
68e6c8
 	if (is_macro)
68e6c8
 		res->u.attr = git_attr_internal(name, namelen);
68e6c8
 	else {
68e6c8
@@ -301,11 +312,11 @@
68e6c8
 
68e6c8
 static void free_attr_elem(struct attr_stack *e)
68e6c8
 {
68e6c8
-	int i;
68e6c8
+	unsigned i;
68e6c8
 	free(e->origin);
68e6c8
 	for (i = 0; i < e->num_matches; i++) {
68e6c8
 		struct match_attr *a = e->attrs[i];
68e6c8
-		int j;
68e6c8
+		size_t j;
68e6c8
 		for (j = 0; j < a->num_attr; j++) {
68e6c8
 			const char *setto = a->state[j].setto;
68e6c8
 			if (setto == ATTR__TRUE ||
68e6c8
@@ -364,20 +375,39 @@
68e6c8
 
68e6c8
 static struct attr_stack *read_attr_from_file(const char *path, int macro_ok)
68e6c8
 {
68e6c8
+	struct strbuf buf = STRBUF_INIT;
68e6c8
 	FILE *fp = fopen(path, "r");
68e6c8
 	struct attr_stack *res;
68e6c8
-	char buf[2048];
68e6c8
 	int lineno = 0;
68e6c8
+	int fd;
68e6c8
+	struct stat st;
68e6c8
 
68e6c8
 	if (!fp) {
68e6c8
 		if (errno != ENOENT && errno != ENOTDIR)
68e6c8
 			warn_on_inaccessible(path);
68e6c8
 		return NULL;
68e6c8
 	}
68e6c8
+	
68e6c8
+	fd = fileno(fp);
68e6c8
+	if (fstat(fd, &st)) {
68e6c8
+		warning_errno(_("cannot fstat gitattributes file '%s'"), path);
68e6c8
+		fclose(fp);
68e6c8
+		return NULL;
68e6c8
+	}
68e6c8
+	if (st.st_size >= ATTR_MAX_FILE_SIZE) {
68e6c8
+		warning(_("ignoring overly large gitattributes file '%s'"), path);
68e6c8
+		fclose(fp);
68e6c8
+		return NULL;
68e6c8
+	}
68e6c8
+
68e6c8
 	res = xcalloc(1, sizeof(*res));
68e6c8
-	while (fgets(buf, sizeof(buf), fp))
68e6c8
-		handle_attr_line(res, buf, path, ++lineno, macro_ok);
68e6c8
+	while (strbuf_getline(&buf, fp, '\n') != EOF) {
68e6c8
+		if (!lineno && starts_with(buf.buf, utf8_bom))
68e6c8
+			strbuf_remove(&buf, 0, strlen(utf8_bom));
68e6c8
+		handle_attr_line(res, buf.buf, path, ++lineno, macro_ok);
68e6c8
+	}
68e6c8
 	fclose(fp);
68e6c8
+	strbuf_release(&buf;;
68e6c8
 	return res;
68e6c8
 }
68e6c8
 
68e6c8
@@ -386,11 +416,18 @@
68e6c8
 	struct attr_stack *res;
68e6c8
 	char *buf, *sp;
68e6c8
 	int lineno = 0;
68e6c8
+	unsigned long size;
68e6c8
 
68e6c8
-	buf = read_blob_data_from_index(use_index ? use_index : &the_index, path, NULL);
68e6c8
+	buf = read_blob_data_from_index(use_index ? use_index : &the_index, path, &size);
68e6c8
 	if (!buf)
68e6c8
 		return NULL;
68e6c8
 
68e6c8
+	if (size >= ATTR_MAX_FILE_SIZE) {
68e6c8
+		warning(_("ignoring overly large gitattributes blob '%s'"), path);
68e6c8
+		return NULL;
68e6c8
+	}
68e6c8
+
68e6c8
+
68e6c8
 	res = xcalloc(1, sizeof(*res));
68e6c8
 	for (sp = buf; *sp; ) {
68e6c8
 		char *ep;
68e6c8
@@ -648,15 +685,15 @@
68e6c8
 
68e6c8
 static int macroexpand_one(int attr_nr, int rem);
68e6c8
 
68e6c8
-static int fill_one(const char *what, struct match_attr *a, int rem)
68e6c8
+static int fill_one(const char *what, const struct match_attr *a, int rem)
68e6c8
 {
68e6c8
 	struct git_attr_check *check = check_all_attr;
68e6c8
-	int i;
68e6c8
+	size_t i;
68e6c8
 
68e6c8
-	for (i = a->num_attr - 1; 0 < rem && 0 <= i; i--) {
68e6c8
-		struct git_attr *attr = a->state[i].attr;
68e6c8
+	for (i = a->num_attr; rem > 0 && i > 0; i--) {
68e6c8
+		const struct git_attr *attr = a->state[i - 1].attr;
68e6c8
 		const char **n = &(check[attr->attr_nr].value);
68e6c8
-		const char *v = a->state[i].setto;
68e6c8
+		const char *v = a->state[i - 1].setto;
68e6c8
 
68e6c8
 		if (*n == ATTR__UNKNOWN) {
68e6c8
 			debug_set(what,
68e6c8
@@ -673,11 +710,11 @@
68e6c8
 static int fill(const char *path, int pathlen, int basename_offset,
68e6c8
 		struct attr_stack *stk, int rem)
68e6c8
 {
68e6c8
-	int i;
68e6c8
+	unsigned i;
68e6c8
 	const char *base = stk->origin ? stk->origin : "";
68e6c8
 
68e6c8
-	for (i = stk->num_matches - 1; 0 < rem && 0 <= i; i--) {
68e6c8
-		struct match_attr *a = stk->attrs[i];
68e6c8
+	for (i = stk->num_matches; 0 < rem && 0 < i; i--) {
68e6c8
+			const struct match_attr *a = stk->attrs[i - 1];
68e6c8
 		if (a->is_macro)
68e6c8
 			continue;
68e6c8
 		if (path_matches(path, pathlen, basename_offset,
68e6c8
@@ -691,14 +728,14 @@
68e6c8
 {
68e6c8
 	struct attr_stack *stk;
68e6c8
 	struct match_attr *a = NULL;
68e6c8
-	int i;
68e6c8
+	unsigned i;
68e6c8
 
68e6c8
 	if (check_all_attr[attr_nr].value != ATTR__TRUE)
68e6c8
 		return rem;
68e6c8
 
68e6c8
 	for (stk = attr_stack; !a && stk; stk = stk->prev)
68e6c8
-		for (i = stk->num_matches - 1; !a && 0 <= i; i--) {
68e6c8
-			struct match_attr *ma = stk->attrs[i];
68e6c8
+		for (i = stk->num_matches; !a && i > 0; i--) {
68e6c8
+			struct match_attr *ma = stk->attrs[i - 1];
68e6c8
 			if (!ma->is_macro)
68e6c8
 				continue;
68e6c8
 			if (ma->u.attr->attr_nr == attr_nr)
68e6c8
diff -ur b/attr.h c/attr.h
68e6c8
--- b/attr.h	2013-06-10 22:01:55.000000000 +0200
68e6c8
+++ c/attr.h	2023-02-21 12:25:42.455029765 +0100
68e6c8
@@ -1,6 +1,18 @@
68e6c8
 #ifndef ATTR_H
68e6c8
 #define ATTR_H
68e6c8
 
68e6c8
+/**
68e6c8
+ * The maximum line length for a gitattributes file. If the line exceeds this
68e6c8
+ * length we will ignore it.
68e6c8
+ */
68e6c8
+#define ATTR_MAX_LINE_LENGTH 2048
68e6c8
+
68e6c8
+ /**
68e6c8
+  * The maximum size of the giattributes file. If the file exceeds this size we
68e6c8
+  * will ignore it.
68e6c8
+  */
68e6c8
+#define ATTR_MAX_FILE_SIZE (100 * 1024 * 1024)
68e6c8
+
68e6c8
 /* An attribute is a pointer to this opaque structure */
68e6c8
 struct git_attr;
68e6c8
 
68e6c8
diff -ur b/git-compat-util.h c/git-compat-util.h
68e6c8
--- b/git-compat-util.h	2023-02-21 11:27:58.038145942 +0100
68e6c8
+++ c/git-compat-util.h	2023-02-21 12:27:18.836638388 +0100
68e6c8
@@ -324,7 +324,9 @@
68e6c8
 extern NORETURN void die(const char *err, ...) __attribute__((format (printf, 1, 2)));
68e6c8
 extern NORETURN void die_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
68e6c8
 extern int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
68e6c8
+extern int error_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
68e6c8
 extern void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
68e6c8
+extern void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
68e6c8
 
68e6c8
 /*
68e6c8
  * Let callers be aware of the constant return value; this can help
68e6c8
@@ -524,8 +526,8 @@
68e6c8
 		    (uintmax_t)a, (uintmax_t)b);
68e6c8
 	return a + b;
68e6c8
 }
68e6c8
-#define st_add3(a,b,c)   st_add((a),st_add((b),(c)))
68e6c8
-#define st_add4(a,b,c,d) st_add((a),st_add3((b),(c),(d)))
68e6c8
+#define st_add3(a,b,c)   st_add(st_add((a),(b)),(c))
68e6c8
+#define st_add4(a,b,c,d) st_add(st_add3((a),(b),(c)),(d))
68e6c8
 
68e6c8
 static inline size_t st_mult(size_t a, size_t b)
68e6c8
 {
68e6c8
diff -ur b/t/t0003-attributes.sh c/t/t0003-attributes.sh
68e6c8
--- b/t/t0003-attributes.sh	2013-06-10 22:01:55.000000000 +0200
68e6c8
+++ c/t/t0003-attributes.sh	2023-02-21 12:30:22.614804084 +0100
68e6c8
@@ -245,39 +245,106 @@
68e6c8
 '
68e6c8
 
68e6c8
 test_expect_success 'setup bare' '
68e6c8
-	git clone --bare . bare.git &&
68e6c8
-	cd bare.git
68e6c8
+	git clone --bare . bare.git
68e6c8
 '
68e6c8
 
68e6c8
 test_expect_success 'bare repository: check that .gitattribute is ignored' '
68e6c8
 	(
68e6c8
-		echo "f	test=f"
68e6c8
-		echo "a/i test=a/i"
68e6c8
-	) >.gitattributes &&
68e6c8
-	attr_check f unspecified &&
68e6c8
-	attr_check a/f unspecified &&
68e6c8
-	attr_check a/c/f unspecified &&
68e6c8
-	attr_check a/i unspecified &&
68e6c8
-	attr_check subdir/a/i unspecified
68e6c8
+		cd bare.git &&
68e6c8
+		(
68e6c8
+			echo "f	test=f"
68e6c8
+			echo "a/i test=a/i"
68e6c8
+		) >.gitattributes &&
68e6c8
+		attr_check f unspecified &&
68e6c8
+		attr_check a/f unspecified &&
68e6c8
+		attr_check a/c/f unspecified &&
68e6c8
+		attr_check a/i unspecified &&
68e6c8
+		attr_check subdir/a/i unspecified
68e6c8
+	)
68e6c8
 '
68e6c8
 
68e6c8
 test_expect_success 'bare repository: check that --cached honors index' '
68e6c8
-	GIT_INDEX_FILE=../.git/index \
68e6c8
-	git check-attr --cached --stdin --all <../stdin-all |
68e6c8
-	sort >actual &&
68e6c8
-	test_cmp ../specified-all actual
68e6c8
+	(
68e6c8
+		cd bare.git &&
68e6c8
+		GIT_INDEX_FILE=../.git/index \
68e6c8
+		git check-attr --cached --stdin --all <../stdin-all |
68e6c8
+		sort >actual &&
68e6c8
+		test_cmp ../specified-all actual
68e6c8
+	)
68e6c8
 '
68e6c8
 
68e6c8
 test_expect_success 'bare repository: test info/attributes' '
68e6c8
 	(
68e6c8
-		echo "f	test=f"
68e6c8
-		echo "a/i test=a/i"
68e6c8
-	) >info/attributes &&
68e6c8
-	attr_check f f &&
68e6c8
-	attr_check a/f f &&
68e6c8
-	attr_check a/c/f f &&
68e6c8
-	attr_check a/i a/i &&
68e6c8
-	attr_check subdir/a/i unspecified
68e6c8
+		cd bare.git &&
68e6c8
+		(
68e6c8
+			echo "f	test=f"
68e6c8
+			echo "a/i test=a/i"
68e6c8
+		) >info/attributes &&
68e6c8
+		attr_check f f &&
68e6c8
+		attr_check a/f f &&
68e6c8
+		attr_check a/c/f f &&
68e6c8
+		attr_check a/i a/i &&
68e6c8
+		attr_check subdir/a/i unspecified
68e6c8
+	)
68e6c8
+'
68e6c8
+
68e6c8
+test_expect_success 'large attributes line ignored in tree' '
68e6c8
+	test_when_finished "rm .gitattributes" &&
68e6c8
+	printf "path %02043d" 1 >.gitattributes &&
68e6c8
+	git check-attr --all path >actual 2>err &&
68e6c8
+	echo "warning: ignoring overly long attributes line 1" >expect &&
68e6c8
+	test_cmp expect err &&
68e6c8
+	test_must_be_empty actual
68e6c8
+'
68e6c8
+
68e6c8
+test_expect_success 'large attributes line ignores trailing content in tree' '
68e6c8
+	test_when_finished "rm .gitattributes" &&
68e6c8
+	# older versions of Git broke lines at 2048 bytes; the 2045 bytes
68e6c8
+	# of 0-padding here is accounting for the three bytes of "a 1", which
68e6c8
+	# would knock "trailing" to the "next" line, where it would be
68e6c8
+	# erroneously parsed.
68e6c8
+	printf "a %02045dtrailing attribute\n" 1 >.gitattributes &&
68e6c8
+	git check-attr --all trailing >actual 2>err &&
68e6c8
+	echo "warning: ignoring overly long attributes line 1" >expect &&
68e6c8
+	test_cmp expect err &&
68e6c8
+	test_must_be_empty actual
68e6c8
+'
68e6c8
+
68e6c8
+test_expect_success EXPENSIVE 'large attributes file ignored in tree' '
68e6c8
+	test_when_finished "rm .gitattributes" &&
68e6c8
+	dd if=/dev/zero of=.gitattributes bs=101M count=1 2>/dev/null &&
68e6c8
+	git check-attr --all path >/dev/null 2>err &&
68e6c8
+	echo "warning: ignoring overly large gitattributes file ${SQ}.gitattributes${SQ}" >expect &&
68e6c8
+	test_cmp expect err
68e6c8
+'
68e6c8
+
68e6c8
+test_expect_success 'large attributes line ignored in index' '
68e6c8
+	test_when_finished "git update-index --remove .gitattributes" &&
68e6c8
+	blob=$(printf "path %02043d" 1 | git hash-object -w --stdin) &&
68e6c8
+	git update-index --add --cacheinfo 100644 $blob .gitattributes &&
68e6c8
+	git check-attr --cached --all path >actual 2>err &&
68e6c8
+	echo "warning: ignoring overly long attributes line 1" >expect &&
68e6c8
+	test_cmp expect err &&
68e6c8
+	test_must_be_empty actual
68e6c8
+'
68e6c8
+
68e6c8
+test_expect_success 'large attributes line ignores trailing content in index' '
68e6c8
+	test_when_finished "git update-index --remove .gitattributes" &&
68e6c8
+	blob=$(printf "a %02045dtrailing attribute\n" 1 | git hash-object -w --stdin) &&
68e6c8
+	git update-index --add --cacheinfo 100644 $blob .gitattributes &&
68e6c8
+	git check-attr --cached --all trailing >actual 2>err &&
68e6c8
+	echo "warning: ignoring overly long attributes line 1" >expect &&
68e6c8
+	test_cmp expect err &&
68e6c8
+	test_must_be_empty actual
68e6c8
+'
68e6c8
+
68e6c8
+test_expect_success EXPENSIVE 'large attributes file ignored in index' '
68e6c8
+	test_when_finished "git update-index --remove .gitattributes" &&
68e6c8
+	blob=$(dd if=/dev/zero bs=101M count=1 2>/dev/null | git hash-object -w --stdin) &&
68e6c8
+	git update-index --add --cacheinfo 100644 $blob .gitattributes &&
68e6c8
+	git check-attr --cached --all path >/dev/null 2>err &&
68e6c8
+	echo "warning: ignoring overly large gitattributes blob ${SQ}.gitattributes${SQ}" >expect &&
68e6c8
+	test_cmp expect err
68e6c8
 '
68e6c8
 
68e6c8
 test_done
68e6c8
diff -ur b/t/test-lib-functions.sh c/t/test-lib-functions.sh
68e6c8
--- b/t/test-lib-functions.sh	2013-06-10 22:01:55.000000000 +0200
68e6c8
+++ c/t/test-lib-functions.sh	2023-02-21 12:31:24.357204323 +0100
68e6c8
@@ -609,6 +609,20 @@
68e6c8
 	$GIT_TEST_CMP "$@"
68e6c8
 }
68e6c8
 
68e6c8
+# Check if the file expected to be empty is indeed empty, and barfs
68e6c8
+# otherwise.
68e6c8
+
68e6c8
+test_must_be_empty () {
68e6c8
+	test "$#" -ne 1 && BUG "1 param"
68e6c8
+	test_path_is_file "$1" &&
68e6c8
+	if test -s "$1"
68e6c8
+	then
68e6c8
+		echo "'$1' is not empty, it contains:"
68e6c8
+		cat "$1"
68e6c8
+		return 1
68e6c8
+	fi
68e6c8
+}
68e6c8
+
68e6c8
 # Tests that its two parameters refer to the same revision
68e6c8
 test_cmp_rev () {
68e6c8
 	git rev-parse --verify "$1" >expect.rev &&
68e6c8
diff -ur b/t/test-lib.sh c/t/test-lib.sh
68e6c8
--- b/t/test-lib.sh	2023-02-21 11:52:24.739202530 +0100
68e6c8
+++ c/t/test-lib.sh	2023-02-21 12:31:52.866389106 +0100
68e6c8
@@ -153,6 +153,9 @@
68e6c8
 LF='
68e6c8
 '
68e6c8
 
68e6c8
+# Single quote
68e6c8
+SQ=\'
68e6c8
+
68e6c8
 export _x05 _x40 _z40 LF
68e6c8
 
68e6c8
 # Each test should start with something like this, after copyright notices:
68e6c8
diff -ur b/usage.c c/usage.c
68e6c8
--- b/usage.c	2013-06-10 22:01:55.000000000 +0200
68e6c8
+++ c/usage.c	2023-02-21 12:32:56.807803579 +0100
68e6c8
@@ -104,6 +104,30 @@
68e6c8
 	va_end(params);
68e6c8
 }
68e6c8
 
68e6c8
+static const char *fmt_with_err(char *buf, int n, const char *fmt)
68e6c8
+{
68e6c8
+	char str_error[256], *err;
68e6c8
+	int i, j;
68e6c8
+
68e6c8
+	err = strerror(errno);
68e6c8
+	for (i = j = 0; err[i] && j < sizeof(str_error) - 1; ) {
68e6c8
+		if ((str_error[j++] = err[i++]) != '%')
68e6c8
+			continue;
68e6c8
+		if (j < sizeof(str_error) - 1) {
68e6c8
+			str_error[j++] = '%';
68e6c8
+		} else {
68e6c8
+			/* No room to double the '%', so we overwrite it with
68e6c8
+			 * '\0' below */
68e6c8
+			j--;
68e6c8
+			break;
68e6c8
+		}
68e6c8
+	}
68e6c8
+	str_error[j] = 0;
68e6c8
+	/* Truncation is acceptable here */
68e6c8
+	snprintf(buf, n, "%s: %s", fmt, str_error);
68e6c8
+	return buf;
68e6c8
+}
68e6c8
+
68e6c8
 void NORETURN die_errno(const char *fmt, ...)
68e6c8
 {
68e6c8
 	va_list params;
68e6c8
@@ -149,6 +173,16 @@
68e6c8
 	return -1;
68e6c8
 }
68e6c8
 
68e6c8
+void warning_errno(const char *warn, ...)
68e6c8
+{
68e6c8
+	char buf[1024];
68e6c8
+	va_list params;
68e6c8
+
68e6c8
+	va_start(params, warn);
68e6c8
+	warn_routine(fmt_with_err(buf, sizeof(buf), warn), params);
68e6c8
+	va_end(params);
68e6c8
+}
68e6c8
+
68e6c8
 void warning(const char *warn, ...)
68e6c8
 {
68e6c8
 	va_list params;
68e6c8
diff -ur b/utf8.c c/utf8.c
68e6c8
--- b/utf8.c	2023-02-21 12:00:28.555925285 +0100
68e6c8
+++ c/utf8.c	2023-02-21 12:33:48.863141018 +0100
68e6c8
@@ -639,3 +639,14 @@
68e6c8
 
68e6c8
 	return chrlen;
68e6c8
 }
68e6c8
+
68e6c8
+const char utf8_bom[] = "\357\273\277";
68e6c8
+
68e6c8
+int skip_utf8_bom(char **text, size_t len)
68e6c8
+{
68e6c8
+	if (len < strlen(utf8_bom) ||
68e6c8
+	    memcmp(*text, utf8_bom, strlen(utf8_bom)))
68e6c8
+		return 0;
68e6c8
+	*text += strlen(utf8_bom);
68e6c8
+	return 1;
68e6c8
+}
68e6c8
diff -ur b/utf8.h c/utf8.h
68e6c8
--- b/utf8.h	2023-02-21 12:00:40.186991497 +0100
68e6c8
+++ c/utf8.h	2023-02-21 12:34:19.536339834 +0100
68e6c8
@@ -12,6 +12,9 @@
68e6c8
 int same_encoding(const char *, const char *);
68e6c8
 int utf8_fprintf(FILE *, const char *, ...);
68e6c8
 
68e6c8
+extern const char utf8_bom[];
68e6c8
+int skip_utf8_bom(char **, size_t);
68e6c8
+
68e6c8
 void strbuf_add_wrapped_text(struct strbuf *buf,
68e6c8
 		const char *text, int indent, int indent2, int width);
68e6c8
 void strbuf_add_wrapped_bytes(struct strbuf *buf, const char *data, int len,