filebuf.c 12.5 KB
Newer Older
1
/*
Edward Thomson committed
2
 * Copyright (C) the libgit2 contributors. All rights reserved.
3
 *
Vicent Marti committed
4 5
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
6 7 8 9 10 11 12
 */
#include "common.h"
#include "filebuf.h"
#include "fileops.h"

static const size_t WRITE_BUFFER_SIZE = (4096 * 2);

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
enum buferr_t {
	BUFERR_OK = 0,
	BUFERR_WRITE,
	BUFERR_ZLIB,
	BUFERR_MEM
};

#define ENSURE_BUF_OK(buf) if ((buf)->last_error != BUFERR_OK) { return -1; }

static int verify_last_error(git_filebuf *file)
{
	switch (file->last_error) {
	case BUFERR_WRITE:
		giterr_set(GITERR_OS, "Failed to write out file");
		return -1;

	case BUFERR_MEM:
		giterr_set_oom();
		return -1;

	case BUFERR_ZLIB:
		giterr_set(GITERR_ZLIB,
			"Buffer error when writing out ZLib data");
		return -1;

	default:
		return 0;
	}
}

43
static int lock_file(git_filebuf *file, int flags, mode_t mode)
44
{
45
	if (git_path_exists(file->path_lock) == true) {
46
		if (flags & GIT_FILEBUF_FORCE)
Vicent Marti committed
47
			p_unlink(file->path_lock);
48
		else {
49
			giterr_clear(); /* actual OS error code just confuses */
50 51
			giterr_set(GITERR_OS,
				"Failed to lock file '%s' for writing", file->path_lock);
52
			return GIT_ELOCKED;
53
		}
54 55
	}

56 57
	/* create path to the file buffer is required */
	if (flags & GIT_FILEBUF_FORCE) {
58
		/* XXX: Should dirmode here be configurable? Or is 0777 always fine? */
59
		file->fd = git_futils_creat_locked_withpath(file->path_lock, 0777, mode);
60
	} else {
61
		file->fd = git_futils_creat_locked(file->path_lock, mode);
62
	}
63 64

	if (file->fd < 0)
65
		return file->fd;
66

67 68
	file->fd_is_open = true;

69
	if ((flags & GIT_FILEBUF_APPEND) && git_path_exists(file->path_original) == true) {
70
		git_file source;
71
		char buffer[FILEIO_BUFSIZE];
Vicent Marti committed
72
		ssize_t read_bytes;
73
		int error = 0;
74

Vicent Marti committed
75
		source = p_open(file->path_original, O_RDONLY);
76 77
		if (source < 0) {
			giterr_set(GITERR_OS,
78 79
				"Failed to open file '%s' for reading",
				file->path_original);
80 81
			return -1;
		}
82

Vicent Marti committed
83
		while ((read_bytes = p_read(source, buffer, sizeof(buffer))) > 0) {
84 85
			if ((error = p_write(file->fd, buffer, read_bytes)) < 0)
				break;
86 87
			if (file->compute_digest)
				git_hash_update(&file->digest, buffer, read_bytes);
88 89
		}

Vicent Marti committed
90
		p_close(source);
Vicent Marti committed
91 92 93 94

		if (read_bytes < 0) {
			giterr_set(GITERR_OS, "Failed to read file '%s'", file->path_original);
			return -1;
95 96 97
		} else if (error < 0) {
			giterr_set(GITERR_OS, "Failed to write file '%s'", file->path_lock);
			return -1;
Vicent Marti committed
98
		}
99 100
	}

101
	return 0;
102 103 104 105
}

void git_filebuf_cleanup(git_filebuf *file)
{
106
	if (file->fd_is_open && file->fd >= 0)
Vicent Marti committed
107
		p_close(file->fd);
108

109
	if (file->created_lock && !file->did_rename && file->path_lock && git_path_exists(file->path_lock))
Vicent Marti committed
110
		p_unlink(file->path_lock);
111

112 113 114
	if (file->compute_digest) {
		git_hash_ctx_cleanup(&file->digest);
		file->compute_digest = 0;
115
	}
116

117 118
	if (file->buffer)
		git__free(file->buffer);
119

120 121 122 123 124 125 126 127 128 129
	/* use the presence of z_buf to decide if we need to deflateEnd */
	if (file->z_buf) {
		git__free(file->z_buf);
		deflateEnd(&file->zs);
	}

	if (file->path_original)
		git__free(file->path_original);
	if (file->path_lock)
		git__free(file->path_lock);
130

131 132
	memset(file, 0x0, sizeof(git_filebuf));
	file->fd = -1;
133 134
}

Vicent Marti committed
135
GIT_INLINE(int) flush_buffer(git_filebuf *file)
136
{
Vicent Marti committed
137 138 139 140
	int result = file->write(file, file->buffer, file->buf_pos);
	file->buf_pos = 0;
	return result;
}
141

142 143 144 145 146
int git_filebuf_flush(git_filebuf *file)
{
	return flush_buffer(file);
}

147
static int write_normal(git_filebuf *file, void *source, size_t len)
Vicent Marti committed
148 149
{
	if (len > 0) {
150 151 152 153 154
		if (p_write(file->fd, (void *)source, len) < 0) {
			file->last_error = BUFERR_WRITE;
			return -1;
		}

155 156
		if (file->compute_digest)
			git_hash_update(&file->digest, source, len);
157 158
	}

159
	return 0;
160 161
}

162
static int write_deflate(git_filebuf *file, void *source, size_t len)
Vicent Marti committed
163 164 165 166
{
	z_stream *zs = &file->zs;

	if (len > 0 || file->flush_mode == Z_FINISH) {
167
		zs->next_in = source;
168
		zs->avail_in = (uInt)len;
Vicent Marti committed
169 170

		do {
171
			size_t have;
Vicent Marti committed
172 173

			zs->next_out = file->z_buf;
174
			zs->avail_out = (uInt)file->buf_size;
Vicent Marti committed
175

176 177 178 179
			if (deflate(zs, file->flush_mode) == Z_STREAM_ERROR) {
				file->last_error = BUFERR_ZLIB;
				return -1;
			}
Vicent Marti committed
180

181
			have = file->buf_size - (size_t)zs->avail_out;
Vicent Marti committed
182

183 184 185 186
			if (p_write(file->fd, file->z_buf, have) < 0) {
				file->last_error = BUFERR_WRITE;
				return -1;
			}
Vicent Marti committed
187

188
		} while (zs->avail_out == 0);
Vicent Marti committed
189 190 191

		assert(zs->avail_in == 0);

192 193
		if (file->compute_digest)
			git_hash_update(&file->digest, source, len);
Vicent Marti committed
194 195
	}

196
	return 0;
Vicent Marti committed
197 198
}

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
#define MAX_SYMLINK_DEPTH 5

static int resolve_symlink(git_buf *out, const char *path)
{
	int i, error, root;
	ssize_t ret;
	struct stat st;
	git_buf curpath = GIT_BUF_INIT, target = GIT_BUF_INIT;

	if ((error = git_buf_grow(&target, GIT_PATH_MAX + 1)) < 0 ||
	    (error = git_buf_puts(&curpath, path)) < 0)
		return error;

	for (i = 0; i < MAX_SYMLINK_DEPTH; i++) {
		error = p_lstat(curpath.ptr, &st);
		if (error < 0 && errno == ENOENT) {
			error = git_buf_puts(out, curpath.ptr);
			goto cleanup;
		}

		if (error < 0) {
			giterr_set(GITERR_OS, "failed to stat '%s'", curpath.ptr);
			error = -1;
			goto cleanup;
		}

		if (!S_ISLNK(st.st_mode)) {
			error = git_buf_puts(out, curpath.ptr);
			goto cleanup;
		}

		ret = p_readlink(curpath.ptr, target.ptr, GIT_PATH_MAX);
		if (ret < 0) {
			giterr_set(GITERR_OS, "failed to read symlink '%s'", curpath.ptr);
			error = -1;
			goto cleanup;
		}

		if (ret == GIT_PATH_MAX) {
			giterr_set(GITERR_INVALID, "symlink target too long");
			error = -1;
			goto cleanup;
		}

		/* readlink(2) won't NUL-terminate for us */
		target.ptr[ret] = '\0';
		target.size = ret;

		root = git_path_root(target.ptr);
		if (root >= 0) {
			if ((error = git_buf_puts(&curpath, target.ptr)) < 0)
				goto cleanup;
		} else {
			git_buf dir = GIT_BUF_INIT;

			if ((error = git_path_dirname_r(&dir, curpath.ptr)) < 0)
				goto cleanup;

			git_buf_swap(&curpath, &dir);
			git_buf_free(&dir);

			if ((error = git_path_apply_relative(&curpath, target.ptr)) < 0)
				goto cleanup;
		}
	}

	giterr_set(GITERR_INVALID, "maximum symlink depth reached");
	error = -1;

cleanup:
	git_buf_free(&curpath);
	git_buf_free(&target);
	return error;
}

274
int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode)
275
{
276
	int compression, error = -1;
277
	size_t path_len, alloc_len;
278

279 280 281 282
	/* opening an already open buffer is a programming error;
	 * assert that this never happens instead of returning
	 * an error code */
	assert(file && path && file->buffer == NULL);
283

284 285
	memset(file, 0x0, sizeof(git_filebuf));

286 287 288
	if (flags & GIT_FILEBUF_DO_NOT_BUFFER)
		file->do_not_buffer = true;

289 290 291
	file->buf_size = WRITE_BUFFER_SIZE;
	file->buf_pos = 0;
	file->fd = -1;
292
	file->last_error = BUFERR_OK;
293

Vicent Marti committed
294
	/* Allocate the main cache buffer */
295 296 297 298
	if (!file->do_not_buffer) {
		file->buffer = git__malloc(file->buf_size);
		GITERR_CHECK_ALLOC(file->buffer);
	}
299

Vicent Marti committed
300 301
	/* If we are hashing on-write, allocate a new hash context */
	if (flags & GIT_FILEBUF_HASH_CONTENTS) {
302
		file->compute_digest = 1;
303

304
		if (git_hash_ctx_init(&file->digest) < 0)
305
			goto cleanup;
306 307
	}

308
	compression = flags >> GIT_FILEBUF_DEFLATE_SHIFT;
309

310 311
	/* If we are deflating on-write, */
	if (compression != 0) {
Vicent Marti committed
312
		/* Initialize the ZLib stream */
313
		if (deflateInit(&file->zs, compression) != Z_OK) {
314
			giterr_set(GITERR_ZLIB, "Failed to initialize zlib");
Vicent Marti committed
315 316
			goto cleanup;
		}
317

Vicent Marti committed
318 319
		/* Allocate the Zlib cache buffer */
		file->z_buf = git__malloc(file->buf_size);
320
		GITERR_CHECK_ALLOC(file->z_buf);
Vicent Marti committed
321 322 323 324 325 326

		/* Never flush */
		file->flush_mode = Z_NO_FLUSH;
		file->write = &write_deflate;
	} else {
		file->write = &write_normal;
327 328
	}

Vicent Marti committed
329 330
	/* If we are writing to a temp file */
	if (flags & GIT_FILEBUF_TEMPORARY) {
331
		git_buf tmp_path = GIT_BUF_INIT;
Vicent Marti committed
332 333

		/* Open the file as temporary for locking */
334
		file->fd = git_futils_mktmp(&tmp_path, path, mode);
335

Vicent Marti committed
336
		if (file->fd < 0) {
337
			git_buf_free(&tmp_path);
Vicent Marti committed
338 339
			goto cleanup;
		}
340
		file->fd_is_open = true;
341
		file->created_lock = true;
Vicent Marti committed
342 343 344

		/* No original path */
		file->path_original = NULL;
345
		file->path_lock = git_buf_detach(&tmp_path);
346
		GITERR_CHECK_ALLOC(file->path_lock);
Vicent Marti committed
347
	} else {
348 349 350 351
		git_buf resolved_path = GIT_BUF_INIT;

		if ((error = resolve_symlink(&resolved_path, path)) < 0)
			goto cleanup;
Vicent Marti committed
352 353

		/* Save the original path of the file */
354 355
		path_len = resolved_path.size;
		file->path_original = git_buf_detach(&resolved_path);
Vicent Marti committed
356 357

		/* create the locking path by appending ".lock" to the original */
358 359
		GITERR_CHECK_ALLOC_ADD(&alloc_len, path_len, GIT_FILELOCK_EXTLENGTH);
		file->path_lock = git__malloc(alloc_len);
360
		GITERR_CHECK_ALLOC(file->path_lock);
Vicent Marti committed
361 362 363 364

		memcpy(file->path_lock, file->path_original, path_len);
		memcpy(file->path_lock + path_len, GIT_FILELOCK_EXTENSION, GIT_FILELOCK_EXTLENGTH);

365 366 367 368 369 370
		if (git_path_isdir(file->path_original)) {
			giterr_set(GITERR_FILESYSTEM, "path '%s' is a directory", file->path_original);
			error = GIT_EDIRECTORY;
			goto cleanup;
		}

Vicent Marti committed
371
		/* open the file for locking */
372
		if ((error = lock_file(file, flags, mode)) < 0)
Vicent Marti committed
373
			goto cleanup;
374 375

		file->created_lock = true;
Vicent Marti committed
376
	}
377

378
	return 0;
379 380 381

cleanup:
	git_filebuf_cleanup(file);
382
	return error;
383 384 385 386
}

int git_filebuf_hash(git_oid *oid, git_filebuf *file)
{
387
	assert(oid && file && file->compute_digest);
388

389 390 391 392
	flush_buffer(file);

	if (verify_last_error(file) < 0)
		return -1;
393

394 395 396
	git_hash_final(oid, &file->digest);
	git_hash_ctx_cleanup(&file->digest);
	file->compute_digest = 0;
397

398
	return 0;
399 400
}

401
int git_filebuf_commit_at(git_filebuf *file, const char *path)
Vicent Marti committed
402
{
403
	git__free(file->path_original);
Vicent Marti committed
404
	file->path_original = git__strdup(path);
405
	GITERR_CHECK_ALLOC(file->path_original);
Vicent Marti committed
406

407
	return git_filebuf_commit(file);
Vicent Marti committed
408 409
}

410
int git_filebuf_commit(git_filebuf *file)
411
{
Vicent Marti committed
412 413
	/* temporary files cannot be committed */
	assert(file && file->path_original);
Vicent Marti committed
414 415

	file->flush_mode = Z_FINISH;
416 417 418 419
	flush_buffer(file);

	if (verify_last_error(file) < 0)
		goto on_error;
420

421
	file->fd_is_open = false;
422

423 424 425 426 427 428 429
	if (p_close(file->fd) < 0) {
		giterr_set(GITERR_OS, "Failed to close file at '%s'", file->path_lock);
		goto on_error;
	}

	file->fd = -1;

430 431 432 433
	if (p_rename(file->path_lock, file->path_original) < 0) {
		giterr_set(GITERR_OS, "Failed to rename lockfile to '%s'", file->path_original);
		goto on_error;
	}
434

435 436
	file->did_rename = true;

437
	git_filebuf_cleanup(file);
438 439 440 441 442
	return 0;

on_error:
	git_filebuf_cleanup(file);
	return -1;
443 444
}

Vicent Marti committed
445
GIT_INLINE(void) add_to_cache(git_filebuf *file, const void *buf, size_t len)
446 447 448 449 450
{
	memcpy(file->buffer + file->buf_pos, buf, len);
	file->buf_pos += len;
}

Vicent Marti committed
451
int git_filebuf_write(git_filebuf *file, const void *buff, size_t len)
452
{
Vicent Marti committed
453
	const unsigned char *buf = buff;
454

455 456
	ENSURE_BUF_OK(file);

457 458 459
	if (file->do_not_buffer)
		return file->write(file, (void *)buff, len);

460 461 462 463 464 465
	for (;;) {
		size_t space_left = file->buf_size - file->buf_pos;

		/* cache if it's small */
		if (space_left > len) {
			add_to_cache(file, buf, len);
466
			return 0;
467 468
		}

469
		add_to_cache(file, buf, space_left);
470 471
		if (flush_buffer(file) < 0)
			return -1;
472

473 474
		len -= space_left;
		buf += space_left;
475 476 477 478 479 480 481 482 483
	}
}

int git_filebuf_reserve(git_filebuf *file, void **buffer, size_t len)
{
	size_t space_left = file->buf_size - file->buf_pos;

	*buffer = NULL;

484 485 486 487 488 489
	ENSURE_BUF_OK(file);

	if (len > file->buf_size) {
		file->last_error = BUFERR_MEM;
		return -1;
	}
490 491

	if (space_left <= len) {
492 493
		if (flush_buffer(file) < 0)
			return -1;
494 495 496 497 498
	}

	*buffer = (file->buffer + file->buf_pos);
	file->buf_pos += len;

499
	return 0;
500 501
}

502 503 504
int git_filebuf_printf(git_filebuf *file, const char *format, ...)
{
	va_list arglist;
505
	size_t space_left, len, alloclen;
506
	int written, res;
507
	char *tmp_buffer;
508

509 510
	ENSURE_BUF_OK(file);

511 512 513 514
	space_left = file->buf_size - file->buf_pos;

	do {
		va_start(arglist, format);
515
		written = p_vsnprintf((char *)file->buffer + file->buf_pos, space_left, format, arglist);
516 517
		va_end(arglist);

518
		if (written < 0) {
519 520 521
			file->last_error = BUFERR_MEM;
			return -1;
		}
522

523 524
		len = written;
		if (len + 1 <= space_left) {
525
			file->buf_pos += len;
526
			return 0;
527
		}
528

529 530
		if (flush_buffer(file) < 0)
			return -1;
531

532 533
		space_left = file->buf_size - file->buf_pos;

534
	} while (len + 1 <= space_left);
535

536 537
	if (GIT_ADD_SIZET_OVERFLOW(&alloclen, len, 1) ||
		!(tmp_buffer = git__malloc(alloclen))) {
538 539 540
		file->last_error = BUFERR_MEM;
		return -1;
	}
541 542

	va_start(arglist, format);
543
	written = p_vsnprintf(tmp_buffer, len + 1, format, arglist);
544 545
	va_end(arglist);

546
	if (written < 0) {
547
		git__free(tmp_buffer);
548 549
		file->last_error = BUFERR_MEM;
		return -1;
550 551
	}

552
	res = git_filebuf_write(file, tmp_buffer, len);
553
	git__free(tmp_buffer);
554

555
	return res;
556 557
}

558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
int git_filebuf_stats(time_t *mtime, size_t *size, git_filebuf *file)
{
	int res;
	struct stat st;

	if (file->fd_is_open)
		res = p_fstat(file->fd, &st);
	else
		res = p_stat(file->path_original, &st);

	if (res < 0) {
		giterr_set(GITERR_OS, "Could not get stat info for '%s'",
			file->path_original);
		return res;
	}

	if (mtime)
		*mtime = st.st_mtime;
	if (size)
		*size = (size_t)st.st_size;

	return 0;
}