Blame src/hashsig.c

Packit ae9e2a
/*
Packit ae9e2a
 * Copyright (C) the libgit2 contributors. All rights reserved.
Packit ae9e2a
 *
Packit ae9e2a
 * This file is part of libgit2, distributed under the GNU GPL v2 with
Packit ae9e2a
 * a Linking Exception. For full terms see the included COPYING file.
Packit ae9e2a
 */
Packit ae9e2a
#include "git2/sys/hashsig.h"
Packit ae9e2a
#include "fileops.h"
Packit ae9e2a
#include "util.h"
Packit ae9e2a
Packit ae9e2a
typedef uint32_t hashsig_t;
Packit ae9e2a
typedef uint64_t hashsig_state;
Packit ae9e2a
Packit ae9e2a
#define HASHSIG_SCALE 100
Packit ae9e2a
Packit ae9e2a
#define HASHSIG_MAX_RUN 80
Packit ae9e2a
#define HASHSIG_HASH_START	0x012345678ABCDEF0LL
Packit ae9e2a
#define HASHSIG_HASH_SHIFT  5
Packit ae9e2a
Packit ae9e2a
#define HASHSIG_HASH_MIX(S,CH) \
Packit ae9e2a
	(S) = ((S) << HASHSIG_HASH_SHIFT) - (S) + (hashsig_state)(CH)
Packit ae9e2a
Packit ae9e2a
#define HASHSIG_HEAP_SIZE ((1 << 7) - 1)
Packit ae9e2a
#define HASHSIG_HEAP_MIN_SIZE 4
Packit ae9e2a
Packit ae9e2a
typedef int (*hashsig_cmp)(const void *a, const void *b, void *);
Packit ae9e2a
Packit ae9e2a
typedef struct {
Packit ae9e2a
	int size, asize;
Packit ae9e2a
	hashsig_cmp cmp;
Packit ae9e2a
	hashsig_t values[HASHSIG_HEAP_SIZE];
Packit ae9e2a
} hashsig_heap;
Packit ae9e2a
Packit ae9e2a
struct git_hashsig {
Packit ae9e2a
	hashsig_heap mins;
Packit ae9e2a
	hashsig_heap maxs;
Packit ae9e2a
	size_t lines;
Packit ae9e2a
	git_hashsig_option_t opt;
Packit ae9e2a
};
Packit ae9e2a
Packit ae9e2a
#define HEAP_LCHILD_OF(I) (((I)<<1)+1)
Packit ae9e2a
#define HEAP_RCHILD_OF(I) (((I)<<1)+2)
Packit ae9e2a
#define HEAP_PARENT_OF(I) (((I)-1)>>1)
Packit ae9e2a
Packit ae9e2a
static void hashsig_heap_init(hashsig_heap *h, hashsig_cmp cmp)
Packit ae9e2a
{
Packit ae9e2a
	h->size  = 0;
Packit ae9e2a
	h->asize = HASHSIG_HEAP_SIZE;
Packit ae9e2a
	h->cmp   = cmp;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static int hashsig_cmp_max(const void *a, const void *b, void *payload)
Packit ae9e2a
{
Packit ae9e2a
	hashsig_t av = *(const hashsig_t *)a, bv = *(const hashsig_t *)b;
Packit ae9e2a
	GIT_UNUSED(payload);
Packit ae9e2a
	return (av < bv) ? -1 : (av > bv) ? 1 : 0;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static int hashsig_cmp_min(const void *a, const void *b, void *payload)
Packit ae9e2a
{
Packit ae9e2a
	hashsig_t av = *(const hashsig_t *)a, bv = *(const hashsig_t *)b;
Packit ae9e2a
	GIT_UNUSED(payload);
Packit ae9e2a
	return (av > bv) ? -1 : (av < bv) ? 1 : 0;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static void hashsig_heap_up(hashsig_heap *h, int el)
Packit ae9e2a
{
Packit ae9e2a
	int parent_el = HEAP_PARENT_OF(el);
Packit ae9e2a
Packit ae9e2a
	while (el > 0 && h->cmp(&h->values[parent_el], &h->values[el], NULL) > 0) {
Packit ae9e2a
		hashsig_t t = h->values[el];
Packit ae9e2a
		h->values[el] = h->values[parent_el];
Packit ae9e2a
		h->values[parent_el] = t;
Packit ae9e2a
Packit ae9e2a
		el = parent_el;
Packit ae9e2a
		parent_el = HEAP_PARENT_OF(el);
Packit ae9e2a
	}
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static void hashsig_heap_down(hashsig_heap *h, int el)
Packit ae9e2a
{
Packit ae9e2a
	hashsig_t v, lv, rv;
Packit ae9e2a
Packit ae9e2a
	/* 'el < h->size / 2' tests if el is bottom row of heap */
Packit ae9e2a
Packit ae9e2a
	while (el < h->size / 2) {
Packit ae9e2a
		int lel = HEAP_LCHILD_OF(el), rel = HEAP_RCHILD_OF(el), swapel;
Packit ae9e2a
Packit ae9e2a
		v  = h->values[el];
Packit ae9e2a
		lv = h->values[lel];
Packit ae9e2a
		rv = h->values[rel];
Packit ae9e2a
Packit ae9e2a
		if (h->cmp(&v, &lv, NULL) < 0 && h->cmp(&v, &rv, NULL) < 0)
Packit ae9e2a
			break;
Packit ae9e2a
Packit ae9e2a
		swapel = (h->cmp(&lv, &rv, NULL) < 0) ? lel : rel;
Packit ae9e2a
Packit ae9e2a
		h->values[el] = h->values[swapel];
Packit ae9e2a
		h->values[swapel] = v;
Packit ae9e2a
Packit ae9e2a
		el = swapel;
Packit ae9e2a
	}
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static void hashsig_heap_sort(hashsig_heap *h)
Packit ae9e2a
{
Packit ae9e2a
	/* only need to do this at the end for signature comparison */
Packit ae9e2a
	git__qsort_r(h->values, h->size, sizeof(hashsig_t), h->cmp, NULL);
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static void hashsig_heap_insert(hashsig_heap *h, hashsig_t val)
Packit ae9e2a
{
Packit ae9e2a
	/* if heap is not full, insert new element */
Packit ae9e2a
	if (h->size < h->asize) {
Packit ae9e2a
		h->values[h->size++] = val;
Packit ae9e2a
		hashsig_heap_up(h, h->size - 1);
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	/* if heap is full, pop top if new element should replace it */
Packit ae9e2a
	else if (h->cmp(&val, &h->values[0], NULL) > 0) {
Packit ae9e2a
		h->size--;
Packit ae9e2a
		h->values[0] = h->values[h->size];
Packit ae9e2a
		hashsig_heap_down(h, 0);
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
typedef struct {
Packit ae9e2a
	int use_ignores;
Packit ae9e2a
	uint8_t ignore_ch[256];
Packit ae9e2a
} hashsig_in_progress;
Packit ae9e2a
Packit ae9e2a
static void hashsig_in_progress_init(
Packit ae9e2a
	hashsig_in_progress *prog, git_hashsig *sig)
Packit ae9e2a
{
Packit ae9e2a
	int i;
Packit ae9e2a
Packit ae9e2a
	/* no more than one can be set */
Packit ae9e2a
	assert(!(sig->opt & GIT_HASHSIG_IGNORE_WHITESPACE) ||
Packit ae9e2a
		   !(sig->opt & GIT_HASHSIG_SMART_WHITESPACE));
Packit ae9e2a
Packit ae9e2a
	if (sig->opt & GIT_HASHSIG_IGNORE_WHITESPACE) {
Packit ae9e2a
		for (i = 0; i < 256; ++i)
Packit ae9e2a
			prog->ignore_ch[i] = git__isspace_nonlf(i);
Packit ae9e2a
		prog->use_ignores = 1;
Packit ae9e2a
	} else if (sig->opt & GIT_HASHSIG_SMART_WHITESPACE) {
Packit ae9e2a
		for (i = 0; i < 256; ++i)
Packit ae9e2a
			prog->ignore_ch[i] = git__isspace(i);
Packit ae9e2a
		prog->use_ignores = 1;
Packit ae9e2a
	} else {
Packit ae9e2a
		memset(prog, 0, sizeof(*prog));
Packit ae9e2a
	}
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static int hashsig_add_hashes(
Packit ae9e2a
	git_hashsig *sig,
Packit ae9e2a
	const uint8_t *data,
Packit ae9e2a
	size_t size,
Packit ae9e2a
	hashsig_in_progress *prog)
Packit ae9e2a
{
Packit ae9e2a
	const uint8_t *scan = data, *end = data + size;
Packit ae9e2a
	hashsig_state state = HASHSIG_HASH_START;
Packit ae9e2a
	int use_ignores = prog->use_ignores, len;
Packit ae9e2a
	uint8_t ch;
Packit ae9e2a
Packit ae9e2a
	while (scan < end) {
Packit ae9e2a
		state = HASHSIG_HASH_START;
Packit ae9e2a
Packit ae9e2a
		for (len = 0; scan < end && len < HASHSIG_MAX_RUN; ) {
Packit ae9e2a
			ch = *scan;
Packit ae9e2a
Packit ae9e2a
			if (use_ignores)
Packit ae9e2a
				for (; scan < end && git__isspace_nonlf(ch); ch = *scan)
Packit ae9e2a
					++scan;
Packit ae9e2a
			else if (sig->opt &
Packit ae9e2a
					 (GIT_HASHSIG_IGNORE_WHITESPACE | GIT_HASHSIG_SMART_WHITESPACE))
Packit ae9e2a
				for (; scan < end && ch == '\r'; ch = *scan)
Packit ae9e2a
					++scan;
Packit ae9e2a
Packit ae9e2a
			/* peek at next character to decide what to do next */
Packit ae9e2a
			if (sig->opt & GIT_HASHSIG_SMART_WHITESPACE)
Packit ae9e2a
				use_ignores = (ch == '\n');
Packit ae9e2a
Packit ae9e2a
			if (scan >= end)
Packit ae9e2a
				break;
Packit ae9e2a
			++scan;
Packit ae9e2a
Packit ae9e2a
			/* check run terminator */
Packit ae9e2a
			if (ch == '\n' || ch == '\0') {
Packit ae9e2a
				sig->lines++;
Packit ae9e2a
				break;
Packit ae9e2a
			}
Packit ae9e2a
Packit ae9e2a
			++len;
Packit ae9e2a
			HASHSIG_HASH_MIX(state, ch);
Packit ae9e2a
		}
Packit ae9e2a
Packit ae9e2a
		if (len > 0) {
Packit ae9e2a
			hashsig_heap_insert(&sig->mins, (hashsig_t)state);
Packit ae9e2a
			hashsig_heap_insert(&sig->maxs, (hashsig_t)state);
Packit ae9e2a
Packit ae9e2a
			while (scan < end && (*scan == '\n' || !*scan))
Packit ae9e2a
				++scan;
Packit ae9e2a
		}
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	prog->use_ignores = use_ignores;
Packit ae9e2a
Packit ae9e2a
	return 0;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static int hashsig_finalize_hashes(git_hashsig *sig)
Packit ae9e2a
{
Packit ae9e2a
	if (sig->mins.size < HASHSIG_HEAP_MIN_SIZE &&
Packit ae9e2a
		!(sig->opt & GIT_HASHSIG_ALLOW_SMALL_FILES)) {
Packit ae9e2a
		giterr_set(GITERR_INVALID,
Packit ae9e2a
			"file too small for similarity signature calculation");
Packit ae9e2a
		return GIT_EBUFS;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	hashsig_heap_sort(&sig->mins);
Packit ae9e2a
	hashsig_heap_sort(&sig->maxs);
Packit ae9e2a
Packit ae9e2a
	return 0;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static git_hashsig *hashsig_alloc(git_hashsig_option_t opts)
Packit ae9e2a
{
Packit ae9e2a
	git_hashsig *sig = git__calloc(1, sizeof(git_hashsig));
Packit ae9e2a
	if (!sig)
Packit ae9e2a
		return NULL;
Packit ae9e2a
Packit ae9e2a
	hashsig_heap_init(&sig->mins, hashsig_cmp_min);
Packit ae9e2a
	hashsig_heap_init(&sig->maxs, hashsig_cmp_max);
Packit ae9e2a
	sig->opt = opts;
Packit ae9e2a
Packit ae9e2a
	return sig;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_hashsig_create(
Packit ae9e2a
	git_hashsig **out,
Packit ae9e2a
	const char *buf,
Packit ae9e2a
	size_t buflen,
Packit ae9e2a
	git_hashsig_option_t opts)
Packit ae9e2a
{
Packit ae9e2a
	int error;
Packit ae9e2a
	hashsig_in_progress prog;
Packit ae9e2a
	git_hashsig *sig = hashsig_alloc(opts);
Packit ae9e2a
	GITERR_CHECK_ALLOC(sig);
Packit ae9e2a
Packit ae9e2a
	hashsig_in_progress_init(&prog, sig);
Packit ae9e2a
Packit ae9e2a
	error = hashsig_add_hashes(sig, (const uint8_t *)buf, buflen, &prog;;
Packit ae9e2a
Packit ae9e2a
	if (!error)
Packit ae9e2a
		error = hashsig_finalize_hashes(sig);
Packit ae9e2a
Packit ae9e2a
	if (!error)
Packit ae9e2a
		*out = sig;
Packit ae9e2a
	else
Packit ae9e2a
		git_hashsig_free(sig);
Packit ae9e2a
Packit ae9e2a
	return error;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_hashsig_create_fromfile(
Packit ae9e2a
	git_hashsig **out,
Packit ae9e2a
	const char *path,
Packit ae9e2a
	git_hashsig_option_t opts)
Packit ae9e2a
{
Packit ae9e2a
	uint8_t buf[0x1000];
Packit ae9e2a
	ssize_t buflen = 0;
Packit ae9e2a
	int error = 0, fd;
Packit ae9e2a
	hashsig_in_progress prog;
Packit ae9e2a
	git_hashsig *sig = hashsig_alloc(opts);
Packit ae9e2a
	GITERR_CHECK_ALLOC(sig);
Packit ae9e2a
Packit ae9e2a
	if ((fd = git_futils_open_ro(path)) < 0) {
Packit ae9e2a
		git__free(sig);
Packit ae9e2a
		return fd;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	hashsig_in_progress_init(&prog, sig);
Packit ae9e2a
Packit ae9e2a
	while (!error) {
Packit ae9e2a
		if ((buflen = p_read(fd, buf, sizeof(buf))) <= 0) {
Packit ae9e2a
			if ((error = (int)buflen) < 0)
Packit ae9e2a
				giterr_set(GITERR_OS,
Packit ae9e2a
					"read error on '%s' calculating similarity hashes", path);
Packit ae9e2a
			break;
Packit ae9e2a
		}
Packit ae9e2a
Packit ae9e2a
		error = hashsig_add_hashes(sig, buf, buflen, &prog;;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	p_close(fd);
Packit ae9e2a
Packit ae9e2a
	if (!error)
Packit ae9e2a
		error = hashsig_finalize_hashes(sig);
Packit ae9e2a
Packit ae9e2a
	if (!error)
Packit ae9e2a
		*out = sig;
Packit ae9e2a
	else
Packit ae9e2a
		git_hashsig_free(sig);
Packit ae9e2a
Packit ae9e2a
	return error;
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
void git_hashsig_free(git_hashsig *sig)
Packit ae9e2a
{
Packit ae9e2a
	git__free(sig);
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
static int hashsig_heap_compare(const hashsig_heap *a, const hashsig_heap *b)
Packit ae9e2a
{
Packit ae9e2a
	int matches = 0, i, j, cmp;
Packit ae9e2a
Packit ae9e2a
	assert(a->cmp == b->cmp);
Packit ae9e2a
Packit ae9e2a
	/* hash heaps are sorted - just look for overlap vs total */
Packit ae9e2a
Packit ae9e2a
	for (i = 0, j = 0; i < a->size && j < b->size; ) {
Packit ae9e2a
		cmp = a->cmp(&a->values[i], &b->values[j], NULL);
Packit ae9e2a
Packit ae9e2a
		if (cmp < 0)
Packit ae9e2a
			++i;
Packit ae9e2a
		else if (cmp > 0)
Packit ae9e2a
			++j;
Packit ae9e2a
		else {
Packit ae9e2a
			++i; ++j; ++matches;
Packit ae9e2a
		}
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	return HASHSIG_SCALE * (matches * 2) / (a->size + b->size);
Packit ae9e2a
}
Packit ae9e2a
Packit ae9e2a
int git_hashsig_compare(const git_hashsig *a, const git_hashsig *b)
Packit ae9e2a
{
Packit ae9e2a
	/* if we have no elements in either file then each file is either
Packit ae9e2a
	 * empty or blank.  if we're ignoring whitespace then the files are
Packit ae9e2a
	 * similar, otherwise they're dissimilar.
Packit ae9e2a
	 */
Packit ae9e2a
	if (a->mins.size == 0 && b->mins.size == 0) {
Packit ae9e2a
		if ((!a->lines && !b->lines) ||
Packit ae9e2a
			(a->opt & GIT_HASHSIG_IGNORE_WHITESPACE))
Packit ae9e2a
			return HASHSIG_SCALE;
Packit ae9e2a
		else
Packit ae9e2a
			return 0;
Packit ae9e2a
	}
Packit ae9e2a
Packit ae9e2a
	/* if we have fewer than the maximum number of elements, then just use
Packit ae9e2a
	 * one array since the two arrays will be the same
Packit ae9e2a
	 */
Packit ae9e2a
	if (a->mins.size < HASHSIG_HEAP_SIZE)
Packit ae9e2a
		return hashsig_heap_compare(&a->mins, &b->mins);
Packit ae9e2a
	else
Packit ae9e2a
		return (hashsig_heap_compare(&a->mins, &b->mins) +
Packit ae9e2a
				hashsig_heap_compare(&a->maxs, &b->maxs)) / 2;
Packit ae9e2a
}