filebuf.c 12.9 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
#include "filebuf.h"
9

10 11 12 13
#include "fileops.h"

static const size_t WRITE_BUFFER_SIZE = (4096 * 2);

14 15 16 17 18 19 20 21 22 23 24 25 26
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:
27
		giterr_set(GITERR_OS, "failed to write out file");
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
		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;
	}
}

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

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

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

68 69
	file->fd_is_open = true;

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

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

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

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

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

102
	return 0;
103 104 105 106
}

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

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

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

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

121 122 123 124 125 126 127 128 129 130
	/* 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);
131

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

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

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

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

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

160
	return 0;
161 162
}

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

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

		do {
172
			size_t have;
Vicent Marti committed
173 174

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

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

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

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

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

		assert(zs->avail_in == 0);

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

197
	return 0;
Vicent Marti committed
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
#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) {
250
			if ((error = git_buf_sets(&curpath, target.ptr)) < 0)
251 252 253 254 255 256 257 258
				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);
259
			git_buf_dispose(&dir);
260 261 262 263 264 265 266 267 268 269

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

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

cleanup:
270 271
	git_buf_dispose(&curpath);
	git_buf_dispose(&target);
272 273 274
	return error;
}

275
int git_filebuf_open(git_filebuf *file, const char *path, int flags, mode_t mode)
276
{
277 278 279 280 281
	return git_filebuf_open_withsize(file, path, flags, mode, WRITE_BUFFER_SIZE);
}

int git_filebuf_open_withsize(git_filebuf *file, const char *path, int flags, mode_t mode, size_t size)
{
282
	int compression, error = -1;
283
	size_t path_len, alloc_len;
284

285 286 287 288
	/* 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);
289

290 291
	memset(file, 0x0, sizeof(git_filebuf));

292 293 294
	if (flags & GIT_FILEBUF_DO_NOT_BUFFER)
		file->do_not_buffer = true;

295 296 297
	if (flags & GIT_FILEBUF_FSYNC)
		file->do_fsync = true;

298
	file->buf_size = size;
299 300
	file->buf_pos = 0;
	file->fd = -1;
301
	file->last_error = BUFERR_OK;
302

Vicent Marti committed
303
	/* Allocate the main cache buffer */
304 305 306 307
	if (!file->do_not_buffer) {
		file->buffer = git__malloc(file->buf_size);
		GITERR_CHECK_ALLOC(file->buffer);
	}
308

Vicent Marti committed
309 310
	/* If we are hashing on-write, allocate a new hash context */
	if (flags & GIT_FILEBUF_HASH_CONTENTS) {
311
		file->compute_digest = 1;
312

313
		if (git_hash_ctx_init(&file->digest) < 0)
314
			goto cleanup;
315 316
	}

317
	compression = flags >> GIT_FILEBUF_DEFLATE_SHIFT;
318

319 320
	/* If we are deflating on-write, */
	if (compression != 0) {
Vicent Marti committed
321
		/* Initialize the ZLib stream */
322
		if (deflateInit(&file->zs, compression) != Z_OK) {
323
			giterr_set(GITERR_ZLIB, "failed to initialize zlib");
Vicent Marti committed
324 325
			goto cleanup;
		}
326

Vicent Marti committed
327 328
		/* Allocate the Zlib cache buffer */
		file->z_buf = git__malloc(file->buf_size);
329
		GITERR_CHECK_ALLOC(file->z_buf);
Vicent Marti committed
330 331 332 333 334 335

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

Vicent Marti committed
338 339
	/* If we are writing to a temp file */
	if (flags & GIT_FILEBUF_TEMPORARY) {
340
		git_buf tmp_path = GIT_BUF_INIT;
Vicent Marti committed
341 342

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

Vicent Marti committed
345
		if (file->fd < 0) {
346
			git_buf_dispose(&tmp_path);
Vicent Marti committed
347 348
			goto cleanup;
		}
349
		file->fd_is_open = true;
350
		file->created_lock = true;
Vicent Marti committed
351 352 353

		/* No original path */
		file->path_original = NULL;
354
		file->path_lock = git_buf_detach(&tmp_path);
355
		GITERR_CHECK_ALLOC(file->path_lock);
Vicent Marti committed
356
	} else {
357 358 359 360
		git_buf resolved_path = GIT_BUF_INIT;

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

		/* Save the original path of the file */
363 364
		path_len = resolved_path.size;
		file->path_original = git_buf_detach(&resolved_path);
Vicent Marti committed
365 366

		/* create the locking path by appending ".lock" to the original */
367 368
		GITERR_CHECK_ALLOC_ADD(&alloc_len, path_len, GIT_FILELOCK_EXTLENGTH);
		file->path_lock = git__malloc(alloc_len);
369
		GITERR_CHECK_ALLOC(file->path_lock);
Vicent Marti committed
370 371 372 373

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

374 375 376 377 378 379
		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
380
		/* open the file for locking */
381
		if ((error = lock_file(file, flags, mode)) < 0)
Vicent Marti committed
382
			goto cleanup;
383 384

		file->created_lock = true;
Vicent Marti committed
385
	}
386

387
	return 0;
388 389 390

cleanup:
	git_filebuf_cleanup(file);
391
	return error;
392 393 394 395
}

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

398 399 400 401
	flush_buffer(file);

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

403 404 405
	git_hash_final(oid, &file->digest);
	git_hash_ctx_cleanup(&file->digest);
	file->compute_digest = 0;
406

407
	return 0;
408 409
}

410
int git_filebuf_commit_at(git_filebuf *file, const char *path)
Vicent Marti committed
411
{
412
	git__free(file->path_original);
Vicent Marti committed
413
	file->path_original = git__strdup(path);
414
	GITERR_CHECK_ALLOC(file->path_original);
Vicent Marti committed
415

416
	return git_filebuf_commit(file);
Vicent Marti committed
417 418
}

419
int git_filebuf_commit(git_filebuf *file)
420
{
Vicent Marti committed
421 422
	/* temporary files cannot be committed */
	assert(file && file->path_original);
Vicent Marti committed
423 424

	file->flush_mode = Z_FINISH;
425 426 427 428
	flush_buffer(file);

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

430
	file->fd_is_open = false;
431

432 433 434 435 436
	if (file->do_fsync && p_fsync(file->fd) < 0) {
		giterr_set(GITERR_OS, "failed to fsync '%s'", file->path_lock);
		goto on_error;
	}

437
	if (p_close(file->fd) < 0) {
438
		giterr_set(GITERR_OS, "failed to close file at '%s'", file->path_lock);
439 440 441 442 443
		goto on_error;
	}

	file->fd = -1;

444
	if (p_rename(file->path_lock, file->path_original) < 0) {
445
		giterr_set(GITERR_OS, "failed to rename lockfile to '%s'", file->path_original);
446 447
		goto on_error;
	}
448

449 450 451
	if (file->do_fsync && git_futils_fsync_parent(file->path_original) < 0)
		goto on_error;

452 453
	file->did_rename = true;

454
	git_filebuf_cleanup(file);
455 456 457 458 459
	return 0;

on_error:
	git_filebuf_cleanup(file);
	return -1;
460 461
}

Vicent Marti committed
462
GIT_INLINE(void) add_to_cache(git_filebuf *file, const void *buf, size_t len)
463 464 465 466 467
{
	memcpy(file->buffer + file->buf_pos, buf, len);
	file->buf_pos += len;
}

Vicent Marti committed
468
int git_filebuf_write(git_filebuf *file, const void *buff, size_t len)
469
{
Vicent Marti committed
470
	const unsigned char *buf = buff;
471

472 473
	ENSURE_BUF_OK(file);

474 475 476
	if (file->do_not_buffer)
		return file->write(file, (void *)buff, len);

477 478 479 480 481 482
	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);
483
			return 0;
484 485
		}

486
		add_to_cache(file, buf, space_left);
487 488
		if (flush_buffer(file) < 0)
			return -1;
489

490 491
		len -= space_left;
		buf += space_left;
492 493 494 495 496 497 498 499 500
	}
}

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

	*buffer = NULL;

501 502 503 504 505 506
	ENSURE_BUF_OK(file);

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

	if (space_left <= len) {
509 510
		if (flush_buffer(file) < 0)
			return -1;
511 512 513 514 515
	}

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

516
	return 0;
517 518
}

519 520 521
int git_filebuf_printf(git_filebuf *file, const char *format, ...)
{
	va_list arglist;
522
	size_t space_left, len, alloclen;
523
	int written, res;
524
	char *tmp_buffer;
525

526 527
	ENSURE_BUF_OK(file);

528 529 530 531
	space_left = file->buf_size - file->buf_pos;

	do {
		va_start(arglist, format);
532
		written = p_vsnprintf((char *)file->buffer + file->buf_pos, space_left, format, arglist);
533 534
		va_end(arglist);

535
		if (written < 0) {
536 537 538
			file->last_error = BUFERR_MEM;
			return -1;
		}
539

540 541
		len = written;
		if (len + 1 <= space_left) {
542
			file->buf_pos += len;
543
			return 0;
544
		}
545

546 547
		if (flush_buffer(file) < 0)
			return -1;
548

549 550
		space_left = file->buf_size - file->buf_pos;

551
	} while (len + 1 <= space_left);
552

553 554
	if (GIT_ADD_SIZET_OVERFLOW(&alloclen, len, 1) ||
		!(tmp_buffer = git__malloc(alloclen))) {
555 556 557
		file->last_error = BUFERR_MEM;
		return -1;
	}
558 559

	va_start(arglist, format);
560
	written = p_vsnprintf(tmp_buffer, len + 1, format, arglist);
561 562
	va_end(arglist);

563
	if (written < 0) {
564
		git__free(tmp_buffer);
565 566
		file->last_error = BUFERR_MEM;
		return -1;
567 568
	}

569
	res = git_filebuf_write(file, tmp_buffer, len);
570
	git__free(tmp_buffer);
571

572
	return res;
573 574
}

575 576 577 578 579 580 581 582 583 584 585
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) {
586
		giterr_set(GITERR_OS, "could not get stat info for '%s'",
587 588 589 590 591 592 593 594 595 596 597
			file->path_original);
		return res;
	}

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

	return 0;
}