path.c 19.6 KB
Newer Older
Vicent Marti committed
1
/*
Edward Thomson committed
2
 * Copyright (C) the libgit2 contributors. All rights reserved.
Vicent Marti committed
3 4 5 6
 *
 * This file is part of libgit2, distributed under the GNU GPL v2 with
 * a Linking Exception. For full terms see the included COPYING file.
 */
Vicent Marti committed
7
#include "common.h"
8 9
#include "path.h"
#include "posix.h"
10
#ifdef GIT_WIN32
11
#include "win32/posix.h"
12 13 14
#else
#include <dirent.h>
#endif
Vicent Marti committed
15 16 17 18
#include <stdarg.h>
#include <stdio.h>
#include <ctype.h>

19 20
#define LOOKS_LIKE_DRIVE_PREFIX(S) (git__isalpha((S)[0]) && (S)[1] == ':')

21
#ifdef GIT_WIN32
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
static bool looks_like_network_computer_name(const char *path, int pos)
{
	if (pos < 3)
		return false;

	if (path[0] != '/' || path[1] != '/')
		return false;

	while (pos-- > 2) {
		if (path[pos] == '/')
			return false;
	}

	return true;
}
37
#endif
38

Vicent Marti committed
39 40
/*
 * Based on the Android implementation, BSD licensed.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
 * http://android.git.kernel.org/
 *
 * Copyright (C) 2008 The Android Open Source Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in
 *   the documentation and/or other materials provided with the
 *   distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
Vicent Marti committed
68
 */
69
int git_path_basename_r(git_buf *buffer, const char *path)
Vicent Marti committed
70 71 72 73 74 75
{
	const char *endp, *startp;
	int len, result;

	/* Empty or NULL string gets treated as "." */
	if (path == NULL || *path == '\0') {
Vicent Marti committed
76 77
		startp = ".";
		len		= 1;
Vicent Marti committed
78 79 80 81 82 83 84 85 86 87 88
		goto Exit;
	}

	/* Strip trailing slashes */
	endp = path + strlen(path) - 1;
	while (endp > path && *endp == '/')
		endp--;

	/* All slashes becomes "/" */
	if (endp == path && *endp == '/') {
		startp = "/";
Vicent Marti committed
89
		len	= 1;
Vicent Marti committed
90 91 92 93 94 95 96 97
		goto Exit;
	}

	/* Find the start of the base */
	startp = endp;
	while (startp > path && *(startp - 1) != '/')
		startp--;

98 99
	/* Cast is safe because max path < max int */
	len = (int)(endp - startp + 1);
Vicent Marti committed
100 101 102 103

Exit:
	result = len;

104 105
	if (buffer != NULL && git_buf_set(buffer, startp, len) < 0)
		return -1;
106

Vicent Marti committed
107 108 109 110 111 112 113
	return result;
}

/*
 * Based on the Android implementation, BSD licensed.
 * Check http://android.git.kernel.org/
 */
114
int git_path_dirname_r(git_buf *buffer, const char *path)
Vicent Marti committed
115
{
Vicent Marti committed
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
	const char *endp;
	int result, len;

	/* Empty or NULL string gets treated as "." */
	if (path == NULL || *path == '\0') {
		path = ".";
		len = 1;
		goto Exit;
	}

	/* Strip trailing slashes */
	endp = path + strlen(path) - 1;
	while (endp > path && *endp == '/')
		endp--;

	/* Find the start of the dir */
	while (endp > path && *endp != '/')
		endp--;

	/* Either the dir is "/" or there are no slashes */
	if (endp == path) {
		path = (*endp == '/') ? "/" : ".";
		len = 1;
		goto Exit;
	}

	do {
		endp--;
	} while (endp > path && *endp == '/');

146 147
	/* Cast is safe because max path < max int */
	len = (int)(endp - path + 1);
Vicent Marti committed
148

Jerome Lambourg committed
149
#ifdef GIT_WIN32
Vicent Marti committed
150 151
	/* Mimic unix behavior where '/.git' returns '/': 'C:/.git' will return
		'C:/' here */
Jerome Lambourg committed
152

153
	if (len == 2 && LOOKS_LIKE_DRIVE_PREFIX(path)) {
Vicent Marti committed
154 155 156
		len = 3;
		goto Exit;
	}
157 158 159 160 161 162 163 164 165

	/* Similarly checks if we're dealing with a network computer name
		'//computername/.git' will return '//computername/' */

	if (looks_like_network_computer_name(path, len)) {
		len++;
		goto Exit;
	}

Jerome Lambourg committed
166 167
#endif

Vicent Marti committed
168
Exit:
Vicent Marti committed
169 170
	result = len;

171 172
	if (buffer != NULL && git_buf_set(buffer, path, len) < 0)
		return -1;
Vicent Marti committed
173 174

	return result;
Vicent Marti committed
175 176 177 178 179
}


char *git_path_dirname(const char *path)
{
180 181
	git_buf buf = GIT_BUF_INIT;
	char *dirname;
Vicent Marti committed
182

183 184 185
	git_path_dirname_r(&buf, path);
	dirname = git_buf_detach(&buf);
	git_buf_free(&buf); /* avoid memleak if error occurs */
Vicent Marti committed
186

187
	return dirname;
Vicent Marti committed
188 189 190 191
}

char *git_path_basename(const char *path)
{
192 193
	git_buf buf = GIT_BUF_INIT;
	char *basename;
Vicent Marti committed
194

195 196 197
	git_path_basename_r(&buf, path);
	basename = git_buf_detach(&buf);
	git_buf_free(&buf); /* avoid memleak if error occurs */
Vicent Marti committed
198

199
	return basename;
Vicent Marti committed
200 201
}

202 203 204 205 206 207 208 209 210 211 212 213 214 215
size_t git_path_basename_offset(git_buf *buffer)
{
	ssize_t slash;

	if (!buffer || buffer->size <= 0)
		return 0;

	slash = git_buf_rfind_next(buffer, '/');

	if (slash >= 0 && buffer->ptr[slash] == '/')
		return (size_t)(slash + 1);

	return 0;
}
Vicent Marti committed
216 217 218 219

const char *git_path_topdir(const char *path)
{
	size_t len;
220
	ssize_t i;
Vicent Marti committed
221 222 223 224 225 226 227

	assert(path);
	len = strlen(path);

	if (!len || path[len - 1] != '/')
		return NULL;

228
	for (i = (ssize_t)len - 2; i >= 0; --i)
Vicent Marti committed
229 230 231 232 233 234
		if (path[i] == '/')
			break;

	return &path[i + 1];
}

235 236 237 238 239
int git_path_root(const char *path)
{
	int offset = 0;

	/* Does the root of the path look like a windows drive ? */
240
	if (LOOKS_LIKE_DRIVE_PREFIX(path))
241
		offset += 2;
242

243
#ifdef GIT_WIN32
244
	/* Are we dealing with a windows network path? */
245 246
	else if ((path[0] == '/' && path[1] == '/' && path[2] != '/') ||
		(path[0] == '\\' && path[1] == '\\' && path[2] != '\\'))
247
	{
248
		offset += 2;
249

250
		/* Skip the computer name segment */
251
		while (path[offset] && path[offset] != '/' && path[offset] != '\\')
252 253
			offset++;
	}
254 255
#endif

256
	if (path[offset] == '/' || path[offset] == '\\')
257 258
		return offset;

259
	return -1;	/* Not a real error - signals that path is not rooted */
260 261
}

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
int git_path_join_unrooted(
	git_buf *path_out, const char *path, const char *base, ssize_t *root_at)
{
	int error, root;

	assert(path && path_out);

	root = git_path_root(path);

	if (base != NULL && root < 0) {
		error = git_buf_joinpath(path_out, base, path);

		if (root_at)
			*root_at = (ssize_t)strlen(base);
	}
	else {
		error = git_buf_sets(path_out, path);

		if (root_at)
			*root_at = (root < 0) ? 0 : (ssize_t)root;
	}

	return error;
}

287
int git_path_prettify(git_buf *path_out, const char *path, const char *base)
288
{
289
	char buf[GIT_PATH_MAX];
290

291
	assert(path && path_out);
292 293 294

	/* construct path if needed */
	if (base != NULL && git_path_root(path) < 0) {
295 296
		if (git_buf_joinpath(path_out, base, path) < 0)
			return -1;
297 298 299
		path = path_out->ptr;
	}

300
	if (p_realpath(path, buf) == NULL) {
301 302
		/* giterr_set resets the errno when dealing with a GITERR_OS kind of error */
		int error = (errno == ENOENT || errno == ENOTDIR) ? GIT_ENOTFOUND : -1;
303
		giterr_set(GITERR_OS, "Failed to resolve path '%s'", path);
304

305
		git_buf_clear(path_out);
306

307
		return error;
308
	}
309

310
	return git_buf_sets(path_out, buf);
311 312
}

313
int git_path_prettify_dir(git_buf *path_out, const char *path, const char *base)
314
{
315
	int error = git_path_prettify(path_out, path, base);
316
	return (error < 0) ? error : git_path_to_dir(path_out);
317
}
318

319 320 321
int git_path_to_dir(git_buf *path)
{
	if (path->asize > 0 &&
nulltoken committed
322 323
		git_buf_len(path) > 0 &&
		path->ptr[git_buf_len(path) - 1] != '/')
324
		git_buf_putc(path, '/');
325

326
	return git_buf_oom(path) ? -1 : 0;
327
}
328 329 330 331 332 333 334 335 336 337 338

void git_path_string_to_dir(char* path, size_t size)
{
	size_t end = strlen(path);

	if (end && path[end - 1] != '/' && end < size) {
		path[end] = '/';
		path[end + 1] = '\0';
	}
}

339 340
int git__percent_decode(git_buf *decoded_out, const char *input)
{
341
	int len, hi, lo, i;
342 343
	assert(decoded_out && input);

344
	len = (int)strlen(input);
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
	git_buf_clear(decoded_out);

	for(i = 0; i < len; i++)
	{
		char c = input[i];

		if (c != '%')
			goto append;

		if (i >= len - 2)
			goto append;

		hi = git__fromhex(input[i + 1]);
		lo = git__fromhex(input[i + 2]);

		if (hi < 0 || lo < 0)
			goto append;

		c = (char)(hi << 4 | lo);
		i += 2;

append:
367 368
		if (git_buf_putc(decoded_out, c) < 0)
			return -1;
369 370
	}

371 372 373 374 375 376 377
	return 0;
}

static int error_invalid_local_file_uri(const char *uri)
{
	giterr_set(GITERR_CONFIG, "'%s' is not a valid local file URI", uri);
	return -1;
378
}
nulltoken committed
379 380 381

int git_path_fromurl(git_buf *local_path_out, const char *file_url)
{
382
	int offset = 0, len;
nulltoken committed
383 384 385 386

	assert(local_path_out && file_url);

	if (git__prefixcmp(file_url, "file://") != 0)
387
		return error_invalid_local_file_uri(file_url);
nulltoken committed
388 389

	offset += 7;
390
	len = (int)strlen(file_url);
nulltoken committed
391 392 393 394 395 396

	if (offset < len && file_url[offset] == '/')
		offset++;
	else if (offset < len && git__prefixcmp(file_url + offset, "localhost/") == 0)
		offset += 10;
	else
397
		return error_invalid_local_file_uri(file_url);
nulltoken committed
398 399

	if (offset >= len || file_url[offset] == '/')
400
		return error_invalid_local_file_uri(file_url);
nulltoken committed
401

402
#ifndef GIT_WIN32
nulltoken committed
403 404 405 406 407
	offset--;	/* A *nix absolute path starts with a forward slash */
#endif

	git_buf_clear(local_path_out);

408
	return git__percent_decode(local_path_out, file_url + offset);
nulltoken committed
409
}
410 411 412 413 414 415 416

int git_path_walk_up(
	git_buf *path,
	const char *ceiling,
	int (*cb)(void *data, git_buf *),
	void *data)
{
417
	int error = 0;
418 419 420 421 422 423 424
	git_buf iter;
	ssize_t stop = 0, scan;
	char oldc = '\0';

	assert(path && cb);

	if (ceiling != NULL) {
425
		if (git__prefixcmp(path->ptr, ceiling) == 0)
426 427
			stop = (ssize_t)strlen(ceiling);
		else
nulltoken committed
428
			stop = git_buf_len(path);
429
	}
nulltoken committed
430
	scan = git_buf_len(path);
431 432

	iter.ptr = path->ptr;
nulltoken committed
433
	iter.size = git_buf_len(path);
434
	iter.asize = path->asize;
435 436

	while (scan >= stop) {
437
		error = cb(data, &iter);
438
		iter.ptr[scan] = oldc;
439 440
		if (error < 0)
			break;
441 442 443 444 445 446 447 448 449
		scan = git_buf_rfind_next(&iter, '/');
		if (scan >= 0) {
			scan++;
			oldc = iter.ptr[scan];
			iter.size = scan;
			iter.ptr[scan] = '\0';
		}
	}

450 451
	if (scan >= 0)
		iter.ptr[scan] = oldc;
452 453 454

	return error;
}
455

456
bool git_path_exists(const char *path)
457 458
{
	assert(path);
459
	return p_access(path, F_OK) == 0;
460 461
}

462
bool git_path_isdir(const char *path)
463 464
{
	struct stat st;
465 466
	if (p_stat(path, &st) < 0)
		return false;
467

468
	return S_ISDIR(st.st_mode) != 0;
469 470
}

471
bool git_path_isfile(const char *path)
472 473 474 475
{
	struct stat st;

	assert(path);
476 477
	if (p_stat(path, &st) < 0)
		return false;
478

479
	return S_ISREG(st.st_mode) != 0;
480 481
}

Ben Straub committed
482 483 484 485
#ifdef GIT_WIN32

bool git_path_is_empty_dir(const char *path)
{
486
	git_buf pathbuf = GIT_BUF_INIT;
Ben Straub committed
487
	HANDLE hFind = INVALID_HANDLE_VALUE;
488
	git_win32_path wbuf;
Ben Straub committed
489 490 491 492 493
	WIN32_FIND_DATAW ffd;
	bool retval = true;

	if (!git_path_isdir(path)) return false;

494
	git_buf_printf(&pathbuf, "%s\\*", path);
495
	git_win32_path_from_c(wbuf, git_buf_cstr(&pathbuf));
496

Ben Straub committed
497
	hFind = FindFirstFileW(wbuf, &ffd);
498 499
	if (INVALID_HANDLE_VALUE == hFind) {
		giterr_set(GITERR_OS, "Couldn't open '%s'", path);
500
		git_buf_free(&pathbuf);
501
		return false;
Ben Straub committed
502
	}
503 504

	do {
505
		if (!git_path_is_dot_or_dotdotW(ffd.cFileName)) {
506
			retval = false;
507
			break;
508 509 510 511 512
		}
	} while (FindNextFileW(hFind, &ffd) != 0);

	FindClose(hFind);
	git_buf_free(&pathbuf);
Ben Straub committed
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
	return retval;
}

#else

bool git_path_is_empty_dir(const char *path)
{
	DIR *dir = NULL;
	struct dirent *e;
	bool retval = true;

	if (!git_path_isdir(path)) return false;

	dir = opendir(path);
	if (!dir) {
		giterr_set(GITERR_OS, "Couldn't open '%s'", path);
		return false;
	}

	while ((e = readdir(dir)) != NULL) {
		if (!git_path_is_dot_or_dotdot(e->d_name)) {
			giterr_set(GITERR_INVALID,
						  "'%s' exists and is not an empty directory", path);
			retval = false;
			break;
		}
	}
	closedir(dir);

	return retval;
}
#endif

546 547 548
int git_path_lstat(const char *path, struct stat *st)
{
	int err = 0;
549

550 551 552 553 554 555
	if (p_lstat(path, st) < 0) {
		err = (errno == ENOENT) ? GIT_ENOTFOUND : -1;
		giterr_set(GITERR_OS, "Failed to stat file '%s'", path);
	}

	return err;
556 557
}

558
static bool _check_dir_contents(
559 560
	git_buf *dir,
	const char *sub,
561
	bool (*predicate)(const char *))
562
{
563
	bool result;
nulltoken committed
564
	size_t dir_size = git_buf_len(dir);
565 566
	size_t sub_size = strlen(sub);

567
	/* leave base valid even if we could not make space for subdir */
568
	if (git_buf_try_grow(dir, dir_size + sub_size + 2, false, false) < 0)
569 570 571
		return false;

	/* save excursion */
572 573
	git_buf_joinpath(dir, dir->ptr, sub);

574
	result = predicate(dir->ptr);
575

576 577
	/* restore path */
	git_buf_truncate(dir, dir_size);
578
	return result;
579 580
}

581
bool git_path_contains(git_buf *dir, const char *item)
582
{
583
	return _check_dir_contents(dir, item, &git_path_exists);
584 585
}

586
bool git_path_contains_dir(git_buf *base, const char *subdir)
587
{
588
	return _check_dir_contents(base, subdir, &git_path_isdir);
589 590
}

591
bool git_path_contains_file(git_buf *base, const char *file)
592
{
593
	return _check_dir_contents(base, file, &git_path_isfile);
594 595 596 597
}

int git_path_find_dir(git_buf *dir, const char *path, const char *base)
{
598
	int error = git_path_join_unrooted(dir, path, base, NULL);
599

600
	if (!error) {
601 602 603 604 605 606
		char buf[GIT_PATH_MAX];
		if (p_realpath(dir->ptr, buf) != NULL)
			error = git_buf_sets(dir, buf);
	}

	/* call dirname if this is not a directory */
607
	if (!error) /* && git_path_isdir(dir->ptr) == false) */
608
		error = git_path_dirname_r(dir, dir->ptr);
609

610
	if (!error)
611 612 613 614 615
		error = git_path_to_dir(dir);

	return error;
}

616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648
int git_path_resolve_relative(git_buf *path, size_t ceiling)
{
	char *base, *to, *from, *next;
	size_t len;

	if (!path || git_buf_oom(path))
		return -1;

	if (ceiling > path->size)
		ceiling = path->size;

	/* recognize drive prefixes, etc. that should not be backed over */
	if (ceiling == 0)
		ceiling = git_path_root(path->ptr) + 1;

	/* recognize URL prefixes that should not be backed over */
	if (ceiling == 0) {
		for (next = path->ptr; *next && git__isalpha(*next); ++next);
		if (next[0] == ':' && next[1] == '/' && next[2] == '/')
			ceiling = (next + 3) - path->ptr;
	}

	base = to = from = path->ptr + ceiling;

	while (*from) {
		for (next = from; *next && *next != '/'; ++next);

		len = next - from;

		if (len == 1 && from[0] == '.')
			/* do nothing with singleton dot */;

		else if (len == 2 && from[0] == '.' && from[1] == '.') {
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675
			/* error out if trying to up one from a hard base */
			if (to == base && ceiling != 0) {
				giterr_set(GITERR_INVALID,
					"Cannot strip root component off url");
				return -1;
			}

			/* no more path segments to strip,
			 * use '../' as a new base path */
			if (to == base) {
				if (*next == '/')
					len++;

				if (to != from)
					memmove(to, from, len);

				to += len;
				/* this is now the base, can't back up from a
				 * relative prefix */
				base = to;
			} else {
				/* back up a path segment */
				while (to > base && to[-1] == '/') to--;
				while (to > base && to[-1] != '/') to--;
			}
		} else {
			if (*next == '/' && *from != '/')
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
				len++;

			if (to != from)
				memmove(to, from, len);

			to += len;
		}

		from += len;

		while (*from == '/') from++;
	}

	*to = '\0';

	path->size = to - path->ptr;

	return 0;
}

int git_path_apply_relative(git_buf *target, const char *relpath)
{
	git_buf_joinpath(target, git_buf_cstr(target), relpath);
	return git_path_resolve_relative(target, 0);
}

702 703
int git_path_cmp(
	const char *name1, size_t len1, int isdir1,
704 705
	const char *name2, size_t len2, int isdir2,
	int (*compare)(const char *, const char *, size_t))
706
{
707
	unsigned char c1, c2;
708
	size_t len = len1 < len2 ? len1 : len2;
709 710
	int cmp;

711
	cmp = compare(name1, name2, len);
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
	if (cmp)
		return cmp;

	c1 = name1[len];
	c2 = name2[len];

	if (c1 == '\0' && isdir1)
		c1 = '/';

	if (c2 == '\0' && isdir2)
		c2 = '/';

	return (c1 < c2) ? -1 : (c1 > c2) ? 1 : 0;
}

727 728 729 730 731 732 733
int git_path_direach(
	git_buf *path,
	int (*fn)(void *, git_buf *),
	void *arg)
{
	ssize_t wd_len;
	DIR *dir;
734
	struct dirent *de, *de_buf;
735

736
	if (git_path_to_dir(path) < 0)
737
		return -1;
738

nulltoken committed
739
	wd_len = git_buf_len(path);
740

741 742
	if ((dir = opendir(path->ptr)) == NULL) {
		giterr_set(GITERR_OS, "Failed to open directory '%s'", path->ptr);
743 744
		return -1;
	}
745

746
#if defined(__sun) || defined(__GNU__)
747 748 749 750 751 752
	de_buf = git__malloc(sizeof(struct dirent) + FILENAME_MAX + 1);
#else
	de_buf = git__malloc(sizeof(struct dirent));
#endif

	while (p_readdir_r(dir, de_buf, &de) == 0 && de != NULL) {
753 754
		int result;

755
		if (git_path_is_dot_or_dotdot(de->d_name))
756 757
			continue;

758 759 760
		if (git_buf_puts(path, de->d_name) < 0) {
			closedir(dir);
			git__free(de_buf);
761
			return -1;
762
		}
763 764 765 766 767

		result = fn(arg, path);

		git_buf_truncate(path, wd_len); /* restore path */

768
		if (result) {
769
			closedir(dir);
770
			git__free(de_buf);
771
			return GIT_EUSER;
772 773 774 775
		}
	}

	closedir(dir);
776
	git__free(de_buf);
777
	return 0;
778
}
779 780 781 782 783 784 785 786 787

int git_path_dirload(
	const char *path,
	size_t prefix_len,
	size_t alloc_extra,
	git_vector *contents)
{
	int error, need_slash;
	DIR *dir;
788
	struct dirent *de, *de_buf;
789 790 791 792 793 794
	size_t path_len;

	assert(path != NULL && contents != NULL);
	path_len = strlen(path);
	assert(path_len > 0 && path_len >= prefix_len);

795 796 797 798
	if ((dir = opendir(path)) == NULL) {
		giterr_set(GITERR_OS, "Failed to open directory '%s'", path);
		return -1;
	}
799

800
#if defined(__sun) || defined(__GNU__)
801 802 803 804 805
	de_buf = git__malloc(sizeof(struct dirent) + FILENAME_MAX + 1);
#else
	de_buf = git__malloc(sizeof(struct dirent));
#endif

806 807 808 809
	path += prefix_len;
	path_len -= prefix_len;
	need_slash = (path_len > 0 && path[path_len-1] != '/') ? 1 : 0;

810
	while ((error = p_readdir_r(dir, de_buf, &de)) == 0 && de != NULL) {
811 812 813
		char *entry_path;
		size_t entry_len;

814
		if (git_path_is_dot_or_dotdot(de->d_name))
815 816 817 818 819 820
			continue;

		entry_len = strlen(de->d_name);

		entry_path = git__malloc(
			path_len + need_slash + entry_len + 1 + alloc_extra);
821
		GITERR_CHECK_ALLOC(entry_path);
822 823 824 825 826 827 828 829

		if (path_len)
			memcpy(entry_path, path, path_len);
		if (need_slash)
			entry_path[path_len] = '/';
		memcpy(&entry_path[path_len + need_slash], de->d_name, entry_len);
		entry_path[path_len + need_slash + entry_len] = '\0';

830 831 832
		if (git_vector_insert(contents, entry_path) < 0) {
			closedir(dir);
			git__free(de_buf);
833
			return -1;
834
		}
835 836 837
	}

	closedir(dir);
838
	git__free(de_buf);
839

840 841
	if (error != 0)
		giterr_set(GITERR_OS, "Failed to process directory entry in '%s'", path);
842

843
	return error;
844 845
}

846 847 848
int git_path_with_stat_cmp(const void *a, const void *b)
{
	const git_path_with_stat *psa = a, *psb = b;
849 850 851 852 853 854 855
	return strcmp(psa->path, psb->path);
}

int git_path_with_stat_cmp_icase(const void *a, const void *b)
{
	const git_path_with_stat *psa = a, *psb = b;
	return strcasecmp(psa->path, psb->path);
856 857 858 859 860
}

int git_path_dirload_with_stat(
	const char *path,
	size_t prefix_len,
861 862 863
	bool ignore_case,
	const char *start_stat,
	const char *end_stat,
864 865 866 867 868 869
	git_vector *contents)
{
	int error;
	unsigned int i;
	git_path_with_stat *ps;
	git_buf full = GIT_BUF_INIT;
870 871 872
	int (*strncomp)(const char *a, const char *b, size_t sz);
	size_t start_len = start_stat ? strlen(start_stat) : 0;
	size_t end_len = end_stat ? strlen(end_stat) : 0, cmp_len;
873

874 875
	if (git_buf_set(&full, path, prefix_len) < 0)
		return -1;
876

877 878 879
	error = git_path_dirload(
		path, prefix_len, sizeof(git_path_with_stat) + 1, contents);
	if (error < 0) {
880 881 882 883
		git_buf_free(&full);
		return error;
	}

884 885 886
	strncomp = ignore_case ? git__strncasecmp : git__strncmp;

	/* stat struct at start of git_path_with_stat, so shift path text */
887 888 889 890
	git_vector_foreach(contents, i, ps) {
		size_t path_len = strlen((char *)ps);
		memmove(ps->path, ps, path_len + 1);
		ps->path_len = path_len;
891 892 893 894 895 896 897 898 899 900
	}

	git_vector_foreach(contents, i, ps) {
		/* skip if before start_stat or after end_stat */
		cmp_len = min(start_len, ps->path_len);
		if (cmp_len && strncomp(ps->path, start_stat, cmp_len) < 0)
			continue;
		cmp_len = min(end_len, ps->path_len);
		if (cmp_len && strncomp(ps->path, end_stat, cmp_len) > 0)
			continue;
901

902 903
		git_buf_truncate(&full, prefix_len);

904
		if ((error = git_buf_joinpath(&full, full.ptr, ps->path)) < 0 ||
905 906 907 908 909 910 911 912
			(error = git_path_lstat(full.ptr, &ps->st)) < 0) {
			if (error == GIT_ENOTFOUND) {
				giterr_clear();
				error = 0;
				git_vector_remove(contents, i--);
				continue;
			}

913
			break;
914
		}
915

916
		if (S_ISDIR(ps->st.st_mode)) {
917 918 919 920 921 922 923 924 925
			if ((error = git_buf_joinpath(&full, full.ptr, ".git")) < 0)
				break;

			if (p_access(full.ptr, F_OK) == 0) {
				ps->st.st_mode = GIT_FILEMODE_COMMIT;
			} else {
				ps->path[ps->path_len++] = '/';
				ps->path[ps->path_len] = '\0';
			}
926 927 928
		}
	}

929 930 931
	/* sort now that directory suffix is added */
	git_vector_sort(contents);

932 933
	git_buf_free(&full);

934 935
	return error;
}