crlf.c 6.98 KB
Newer Older
1
/*
Edward Thomson committed
2
 * Copyright (C) the libgit2 contributors. All rights reserved.
3 4 5 6 7
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */

8 9 10
#include "git2/attr.h"
#include "git2/blob.h"
#include "git2/index.h"
11
#include "git2/sys/filter.h"
12

13 14 15 16
#include "common.h"
#include "fileops.h"
#include "hash.h"
#include "filter.h"
17
#include "buf_text.h"
18 19 20 21 22
#include "repository.h"

struct crlf_attrs {
	int crlf_action;
	int eol;
23
	int auto_crlf;
24 25 26 27 28 29 30 31
};

struct crlf_filter {
	git_filter f;
};

static int check_crlf(const char *value)
{
32
	if (GIT_ATTR_TRUE(value))
33 34
		return GIT_CRLF_TEXT;

35
	if (GIT_ATTR_FALSE(value))
36 37
		return GIT_CRLF_BINARY;

38
	if (GIT_ATTR_UNSPECIFIED(value))
39 40 41 42 43 44 45 46 47 48 49 50 51
		return GIT_CRLF_GUESS;

	if (strcmp(value, "input") == 0)
		return GIT_CRLF_INPUT;

	if (strcmp(value, "auto") == 0)
		return GIT_CRLF_AUTO;

	return GIT_CRLF_GUESS;
}

static int check_eol(const char *value)
{
52
	if (GIT_ATTR_UNSPECIFIED(value))
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
		return GIT_EOL_UNSET;

	if (strcmp(value, "lf") == 0)
		return GIT_EOL_LF;

	if (strcmp(value, "crlf") == 0)
		return GIT_EOL_CRLF;

	return GIT_EOL_UNSET;
}

static int crlf_input_action(struct crlf_attrs *ca)
{
	if (ca->crlf_action == GIT_CRLF_BINARY)
		return GIT_CRLF_BINARY;

	if (ca->eol == GIT_EOL_LF)
		return GIT_CRLF_INPUT;

	if (ca->eol == GIT_EOL_CRLF)
		return GIT_CRLF_CRLF;

	return ca->crlf_action;
}

78
static int has_cr_in_index(const git_filter_source *src)
79
{
80 81
	git_repository *repo = git_filter_source_repo(src);
	const char *path = git_filter_source_path(src);
82 83 84 85 86 87 88
	git_index *index;
	const git_index_entry *entry;
	git_blob *blob;
	const void *blobcontent;
	git_off_t blobsize;
	bool found_cr;

89 90 91
	if (!path)
		return false;

92
	if (git_repository_index__weakptr(&index, repo) < 0) {
93 94 95 96
		giterr_clear();
		return false;
	}

97 98
	if (!(entry = git_index_get_bypath(index, path, 0)) &&
		!(entry = git_index_get_bypath(index, path, 1)))
99 100 101 102 103
		return false;

	if (!S_ISREG(entry->mode)) /* don't crlf filter non-blobs */
		return true;

104
	if (git_blob_lookup(&blob, repo, &entry->oid) < 0)
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
		return false;

	blobcontent = git_blob_rawcontent(blob);
	blobsize    = git_blob_rawsize(blob);
	if (!git__is_sizet(blobsize))
		blobsize = (size_t)-1;

	found_cr = (blobcontent != NULL &&
		blobsize > 0 &&
		memchr(blobcontent, '\r', (size_t)blobsize) != NULL);

	git_blob_free(blob);
	return found_cr;
}

static int crlf_apply_to_odb(
121
	struct crlf_attrs *ca,
122 123
	git_buf *to,
	const git_buf *from,
124
	const git_filter_source *src)
125 126
{
	/* Empty file? Nothing to do */
127
	if (!git_buf_len(from))
128 129 130 131 132
		return 0;

	/* Heuristics to see if we can skip the conversion.
	 * Straight from Core Git.
	 */
133
	if (ca->crlf_action == GIT_CRLF_AUTO || ca->crlf_action == GIT_CRLF_GUESS) {
134 135
		git_buf_text_stats stats;

136
		/* Check heuristics for binary vs text - returns true if binary */
137
		if (git_buf_text_gather_stats(&stats, from, false))
138
			return GIT_PASSTHROUGH;
139 140 141 142 143 144 145

		/*
		 * We're currently not going to even try to convert stuff
		 * that has bare CR characters. Does anybody do that crazy
		 * stuff?
		 */
		if (stats.cr != stats.crlf)
Russell Belfer committed
146
			return GIT_PASSTHROUGH;
147

148
		if (ca->crlf_action == GIT_CRLF_GUESS) {
149 150 151 152
			/*
			 * If the file in the index has any CR in it, do not convert.
			 * This is the new safer autocrlf handling.
			 */
153
			if (has_cr_in_index(src))
Russell Belfer committed
154
				return GIT_PASSTHROUGH;
155 156 157
		}

		if (!stats.cr)
Russell Belfer committed
158
			return GIT_PASSTHROUGH;
159 160
	}

161
	/* Actually drop the carriage returns */
162
	return git_buf_text_crlf_to_lf(to, from);
Ben Straub committed
163 164
}

165
static const char *line_ending(struct crlf_attrs *ca)
Ben Straub committed
166
{
167
	switch (ca->crlf_action) {
Ben Straub committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
	case GIT_CRLF_BINARY:
	case GIT_CRLF_INPUT:
		return "\n";

	case GIT_CRLF_CRLF:
		return "\r\n";

	case GIT_CRLF_AUTO:
	case GIT_CRLF_TEXT:
	case GIT_CRLF_GUESS:
		break;

	default:
		goto line_ending_error;
	}

184
	switch (ca->eol) {
Ben Straub committed
185
	case GIT_EOL_UNSET:
186
		return GIT_EOL_NATIVE == GIT_EOL_CRLF ? "\r\n" : "\n";
Ben Straub committed
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202

	case GIT_EOL_CRLF:
		return "\r\n";

	case GIT_EOL_LF:
		return "\n";

	default:
		goto line_ending_error;
	}

line_ending_error:
	giterr_set(GITERR_INVALID, "Invalid input to line ending filter");
	return NULL;
}

203
static int crlf_apply_to_workdir(
204
	struct crlf_attrs *ca, git_buf *to, const git_buf *from)
Ben Straub committed
205 206 207 208
{
	const char *workdir_ending = NULL;

	/* Empty file? Nothing to do. */
209
	if (git_buf_len(from) == 0)
210
		return 0;
Ben Straub committed
211

212 213
	/* Don't filter binary files */
	if (git_buf_text_is_binary(from))
Russell Belfer committed
214
		return GIT_PASSTHROUGH;
215

Ben Straub committed
216
	/* Determine proper line ending */
217
	workdir_ending = line_ending(ca);
218 219
	if (!workdir_ending)
		return -1;
Ben Straub committed
220

221
	if (!strcmp("\n", workdir_ending)) {
222
		if (ca->crlf_action == GIT_CRLF_GUESS && ca->auto_crlf)
Russell Belfer committed
223
			return GIT_PASSTHROUGH;
224

225
		if (git_buf_find(from, '\r') < 0)
Russell Belfer committed
226
			return GIT_PASSTHROUGH;
227

228
		if (git_buf_text_crlf_to_lf(to, from) < 0)
229 230 231 232 233
			return -1;
	} else {
		/* only other supported option is lf->crlf conversion */
		assert(!strcmp("\r\n", workdir_ending));

234
		if (git_buf_text_lf_to_crlf(to, from) < 0)
235 236 237
			return -1;
	}

238
	return 0;
Ben Straub committed
239 240
}

241 242 243
static int crlf_check(
	git_filter        *self,
	void              **payload, /* points to NULL ptr on entry, may be set */
244 245
	const git_filter_source *src,
	const char **attr_values)
246 247
{
	int error;
248 249 250
	struct crlf_attrs ca;

	GIT_UNUSED(self);
251

252 253 254 255 256 257 258 259 260 261
	if (!attr_values) {
		ca.crlf_action = GIT_CRLF_GUESS;
		ca.eol = GIT_EOL_UNSET;
	} else {
		ca.crlf_action = check_crlf(attr_values[2]); /* text */
		if (ca.crlf_action == GIT_CRLF_GUESS)
			ca.crlf_action = check_crlf(attr_values[0]); /* clrf */
		ca.eol = check_eol(attr_values[1]); /* eol */
	}
	ca.auto_crlf = GIT_AUTO_CRLF_DEFAULT;
262

263 264
	/*
	 * Use the core Git logic to see if we should perform CRLF for this file
265
	 * based on its attributes & the value of `core.autocrlf`
266 267
	 */
	ca.crlf_action = crlf_input_action(&ca);
268

269
	if (ca.crlf_action == GIT_CRLF_BINARY)
Russell Belfer committed
270
		return GIT_PASSTHROUGH;
271

272
	if (ca.crlf_action == GIT_CRLF_GUESS) {
273 274 275
		error = git_repository__cvar(
			&ca.auto_crlf, git_filter_source_repo(src), GIT_CVAR_AUTO_CRLF);
		if (error < 0)
276 277
			return error;

278
		if (ca.auto_crlf == GIT_AUTO_CRLF_FALSE)
Russell Belfer committed
279
			return GIT_PASSTHROUGH;
280
	}
281

282 283 284
	*payload = git__malloc(sizeof(ca));
	GITERR_CHECK_ALLOC(*payload);
	memcpy(*payload, &ca, sizeof(ca));
285

286 287 288 289
	return 0;
}

static int crlf_apply(
290 291 292 293
	git_filter    *self,
	void         **payload, /* may be read and/or set */
	git_buf       *to,
	const git_buf *from,
294 295
	const git_filter_source *src)
{
296 297 298
	/* initialize payload in case `check` was bypassed */
	if (!*payload) {
		int error = crlf_check(self, payload, src, NULL);
Russell Belfer committed
299
		if (error < 0 && error != GIT_PASSTHROUGH)
300 301
			return error;
	}
302

303
	if (git_filter_source_mode(src) == GIT_FILTER_SMUDGE)
304 305 306
		return crlf_apply_to_workdir(*payload, to, from);
	else
		return crlf_apply_to_odb(*payload, to, from, src);
307 308
}

309 310 311
static void crlf_cleanup(
	git_filter *self,
	void       *payload)
312
{
313 314
	GIT_UNUSED(self);
	git__free(payload);
315 316
}

317
git_filter *git_crlf_filter_new(void)
318
{
319
	struct crlf_filter *f = git__calloc(1, sizeof(struct crlf_filter));
320

321
	f->f.version = GIT_FILTER_VERSION;
322 323
	f->f.attributes = "crlf eol text";
	f->f.initialize = NULL;
Russell Belfer committed
324
	f->f.shutdown = git_filter_free;
325 326 327
	f->f.check    = crlf_check;
	f->f.apply    = crlf_apply;
	f->f.cleanup  = crlf_cleanup;
328

329
	return (git_filter *)f;
330
}