path.c 19.1 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 11
#ifdef GIT_WIN32
#include "win32/dir.h"
12
#include "win32/posix.h"
13 14 15
#else
#include <dirent.h>
#endif
Vicent Marti committed
16 17 18 19
#include <stdarg.h>
#include <stdio.h>
#include <ctype.h>

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

22
#ifdef GIT_WIN32
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
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;
}
38
#endif
39

Vicent Marti committed
40 41
/*
 * Based on the Android implementation, BSD licensed.
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 68
 * 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
69
 */
70
int git_path_basename_r(git_buf *buffer, const char *path)
Vicent Marti committed
71 72 73 74 75 76
{
	const char *endp, *startp;
	int len, result;

	/* Empty or NULL string gets treated as "." */
	if (path == NULL || *path == '\0') {
Vicent Marti committed
77 78
		startp = ".";
		len		= 1;
Vicent Marti committed
79 80 81 82 83 84 85 86 87 88 89
		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
90
		len	= 1;
Vicent Marti committed
91 92 93 94 95 96 97 98
		goto Exit;
	}

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

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

Exit:
	result = len;

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

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

/*
 * Based on the Android implementation, BSD licensed.
 * Check http://android.git.kernel.org/
 */
115
int git_path_dirname_r(git_buf *buffer, const char *path)
Vicent Marti committed
116
{
Vicent Marti committed
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 146
	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 == '/');

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

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

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

	/* 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
167 168
#endif

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

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

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


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

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

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

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

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

200
	return basename;
Vicent Marti committed
201 202
}

203 204 205 206 207 208 209 210 211 212 213 214 215 216
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
217 218 219 220

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

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

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

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

	return &path[i + 1];
}

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

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

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

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

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

260
	return -1;	/* Not a real error - signals that path is not rooted */
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 287
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;
}

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

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

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

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

306
		git_buf_clear(path_out);
307

308
		return error;
309
	}
310

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

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

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

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

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';
	}
}

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

345
	len = (int)strlen(input);
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
	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:
368 369
		if (git_buf_putc(decoded_out, c) < 0)
			return -1;
370 371
	}

372 373 374 375 376 377 378
	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;
379
}
nulltoken committed
380 381 382

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

	assert(local_path_out && file_url);

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

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

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

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

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

	git_buf_clear(local_path_out);

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

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

	assert(path && cb);

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

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

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

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

	return error;
}
456

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

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

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

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

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

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

Ben Straub committed
483 484 485 486
#ifdef GIT_WIN32

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

	if (!git_path_isdir(path)) return false;

495
	git_buf_printf(&pathbuf, "%s\\*", path);
496
	git__utf8_to_16(wbuf, GIT_WIN_PATH, git_buf_cstr(&pathbuf));
497

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

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

	FindClose(hFind);
	git_buf_free(&pathbuf);
Ben Straub committed
512 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
	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

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

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

	return err;
555 556
}

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

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

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

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

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

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

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

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

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

599
	if (!error) {
600 601 602 603 604 605
		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 */
606 607
	if (!error && git_path_isdir(dir->ptr) == false)
		error = git_path_dirname_r(dir, dir->ptr);
608

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

	return error;
}

615 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 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 676 677 678 679
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] == '.') {
			while (to > base && to[-1] == '/') to--;
			while (to > base && to[-1] != '/') to--;
		}

		else {
			if (*next == '/')
				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);
}

680 681 682
int git_path_cmp(
	const char *name1, size_t len1, int isdir1,
	const char *name2, size_t len2, int isdir2)
683
{
684
	unsigned char c1, c2;
685
	size_t len = len1 < len2 ? len1 : len2;
686 687 688 689 690
	int cmp;

	cmp = memcmp(name1, name2, len);
	if (cmp)
		return cmp;
691 692 693 694 695 696 697 698 699 700 701

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

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

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

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

704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
int git_path_icmp(
	const char *name1, size_t len1, int isdir1,
	const char *name2, size_t len2, int isdir2)
{
	unsigned char c1, c2;
	size_t len = len1 < len2 ? len1 : len2;
	int cmp;

	cmp = strncasecmp(name1, name2, len);
	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;
}

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

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

nulltoken committed
740
	wd_len = git_buf_len(path);
741

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

747
#if defined(__sun) || defined(__GNU__)
748 749 750 751 752 753
	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) {
754 755
		int result;

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

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

		result = fn(arg, path);

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

769
		if (result < 0) {
770
			closedir(dir);
771
			git__free(de_buf);
772
			return -1;
773 774 775 776
		}
	}

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

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

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

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

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

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

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

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

		entry_len = strlen(de->d_name);

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

		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';

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

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

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

844
	return error;
845 846
}

847 848 849
int git_path_with_stat_cmp(const void *a, const void *b)
{
	const git_path_with_stat *psa = a, *psb = b;
850 851 852 853 854 855 856
	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);
857 858 859 860 861
}

int git_path_dirload_with_stat(
	const char *path,
	size_t prefix_len,
862 863 864
	bool ignore_case,
	const char *start_stat,
	const char *end_stat,
865 866 867 868 869 870
	git_vector *contents)
{
	int error;
	unsigned int i;
	git_path_with_stat *ps;
	git_buf full = GIT_BUF_INIT;
871 872 873
	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;
874

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

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

885 886 887
	strncomp = ignore_case ? git__strncasecmp : git__strncmp;

	/* stat struct at start of git_path_with_stat, so shift path text */
888 889 890 891
	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;
892 893 894 895 896 897 898 899 900 901
	}

	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;
902

903 904
		if ((error = git_buf_joinpath(&full, full.ptr, ps->path)) < 0 ||
			(error = git_path_lstat(full.ptr, &ps->st)) < 0)
905 906
			break;

907 908 909
		git_buf_truncate(&full, prefix_len);

		if (S_ISDIR(ps->st.st_mode)) {
910 911
			ps->path[ps->path_len++] = '/';
			ps->path[ps->path_len] = '\0';
912 913 914
		}
	}

915 916 917
	/* sort now that directory suffix is added */
	git_vector_sort(contents);

918 919
	git_buf_free(&full);

920 921
	return error;
}