
The next change will start allowing us to parse bundle lists that are downloaded from a provided bundle URI. Those lists might point to other lists, which could proceed to an arbitrary depth (and even create cycles). Restructure fetch_bundle_uri() to have an internal version that has a recursion depth. Compare that to a new max_bundle_uri_depth constant that is twice as high as we expect this depth to be for any legitimate use of bundle list linking. We can consider making max_bundle_uri_depth a configurable value if there is demonstrated value in the future. Signed-off-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
413 lines
9.0 KiB
C
413 lines
9.0 KiB
C
#include "cache.h"
|
|
#include "bundle-uri.h"
|
|
#include "bundle.h"
|
|
#include "object-store.h"
|
|
#include "refs.h"
|
|
#include "run-command.h"
|
|
#include "hashmap.h"
|
|
#include "pkt-line.h"
|
|
#include "config.h"
|
|
|
|
static int compare_bundles(const void *hashmap_cmp_fn_data,
|
|
const struct hashmap_entry *he1,
|
|
const struct hashmap_entry *he2,
|
|
const void *id)
|
|
{
|
|
const struct remote_bundle_info *e1 =
|
|
container_of(he1, const struct remote_bundle_info, ent);
|
|
const struct remote_bundle_info *e2 =
|
|
container_of(he2, const struct remote_bundle_info, ent);
|
|
|
|
return strcmp(e1->id, id ? (const char *)id : e2->id);
|
|
}
|
|
|
|
void init_bundle_list(struct bundle_list *list)
|
|
{
|
|
memset(list, 0, sizeof(*list));
|
|
|
|
/* Implied defaults. */
|
|
list->mode = BUNDLE_MODE_ALL;
|
|
list->version = 1;
|
|
|
|
hashmap_init(&list->bundles, compare_bundles, NULL, 0);
|
|
}
|
|
|
|
static int clear_remote_bundle_info(struct remote_bundle_info *bundle,
|
|
void *data)
|
|
{
|
|
FREE_AND_NULL(bundle->id);
|
|
FREE_AND_NULL(bundle->uri);
|
|
return 0;
|
|
}
|
|
|
|
void clear_bundle_list(struct bundle_list *list)
|
|
{
|
|
if (!list)
|
|
return;
|
|
|
|
for_all_bundles_in_list(list, clear_remote_bundle_info, NULL);
|
|
hashmap_clear_and_free(&list->bundles, struct remote_bundle_info, ent);
|
|
}
|
|
|
|
int for_all_bundles_in_list(struct bundle_list *list,
|
|
bundle_iterator iter,
|
|
void *data)
|
|
{
|
|
struct remote_bundle_info *info;
|
|
struct hashmap_iter i;
|
|
|
|
hashmap_for_each_entry(&list->bundles, &i, info, ent) {
|
|
int result = iter(info, data);
|
|
|
|
if (result)
|
|
return result;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int summarize_bundle(struct remote_bundle_info *info, void *data)
|
|
{
|
|
FILE *fp = data;
|
|
fprintf(fp, "[bundle \"%s\"]\n", info->id);
|
|
fprintf(fp, "\turi = %s\n", info->uri);
|
|
return 0;
|
|
}
|
|
|
|
void print_bundle_list(FILE *fp, struct bundle_list *list)
|
|
{
|
|
const char *mode;
|
|
|
|
switch (list->mode) {
|
|
case BUNDLE_MODE_ALL:
|
|
mode = "all";
|
|
break;
|
|
|
|
case BUNDLE_MODE_ANY:
|
|
mode = "any";
|
|
break;
|
|
|
|
case BUNDLE_MODE_NONE:
|
|
default:
|
|
mode = "<unknown>";
|
|
}
|
|
|
|
fprintf(fp, "[bundle]\n");
|
|
fprintf(fp, "\tversion = %d\n", list->version);
|
|
fprintf(fp, "\tmode = %s\n", mode);
|
|
|
|
for_all_bundles_in_list(list, summarize_bundle, fp);
|
|
}
|
|
|
|
/**
|
|
* Given a key-value pair, update the state of the given bundle list.
|
|
* Returns 0 if the key-value pair is understood. Returns -1 if the key
|
|
* is not understood or the value is malformed.
|
|
*/
|
|
static int bundle_list_update(const char *key, const char *value,
|
|
struct bundle_list *list)
|
|
{
|
|
struct strbuf id = STRBUF_INIT;
|
|
struct remote_bundle_info lookup = REMOTE_BUNDLE_INFO_INIT;
|
|
struct remote_bundle_info *bundle;
|
|
const char *subsection, *subkey;
|
|
size_t subsection_len;
|
|
|
|
if (parse_config_key(key, "bundle", &subsection, &subsection_len, &subkey))
|
|
return -1;
|
|
|
|
if (!subsection_len) {
|
|
if (!strcmp(subkey, "version")) {
|
|
int version;
|
|
if (!git_parse_int(value, &version))
|
|
return -1;
|
|
if (version != 1)
|
|
return -1;
|
|
|
|
list->version = version;
|
|
return 0;
|
|
}
|
|
|
|
if (!strcmp(subkey, "mode")) {
|
|
if (!strcmp(value, "all"))
|
|
list->mode = BUNDLE_MODE_ALL;
|
|
else if (!strcmp(value, "any"))
|
|
list->mode = BUNDLE_MODE_ANY;
|
|
else
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* Ignore other unknown global keys. */
|
|
return 0;
|
|
}
|
|
|
|
strbuf_add(&id, subsection, subsection_len);
|
|
|
|
/*
|
|
* Check for an existing bundle with this <id>, or create one
|
|
* if necessary.
|
|
*/
|
|
lookup.id = id.buf;
|
|
hashmap_entry_init(&lookup.ent, strhash(lookup.id));
|
|
if (!(bundle = hashmap_get_entry(&list->bundles, &lookup, ent, NULL))) {
|
|
CALLOC_ARRAY(bundle, 1);
|
|
bundle->id = strbuf_detach(&id, NULL);
|
|
hashmap_entry_init(&bundle->ent, strhash(bundle->id));
|
|
hashmap_add(&list->bundles, &bundle->ent);
|
|
}
|
|
strbuf_release(&id);
|
|
|
|
if (!strcmp(subkey, "uri")) {
|
|
if (bundle->uri)
|
|
return -1;
|
|
bundle->uri = xstrdup(value);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* At this point, we ignore any information that we don't
|
|
* understand, assuming it to be hints for a heuristic the client
|
|
* does not currently understand.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
static int config_to_bundle_list(const char *key, const char *value, void *data)
|
|
{
|
|
struct bundle_list *list = data;
|
|
return bundle_list_update(key, value, list);
|
|
}
|
|
|
|
int bundle_uri_parse_config_format(const char *uri,
|
|
const char *filename,
|
|
struct bundle_list *list)
|
|
{
|
|
int result;
|
|
struct config_options opts = {
|
|
.error_action = CONFIG_ERROR_ERROR,
|
|
};
|
|
|
|
result = git_config_from_file_with_options(config_to_bundle_list,
|
|
filename, list,
|
|
&opts);
|
|
|
|
if (!result && list->mode == BUNDLE_MODE_NONE) {
|
|
warning(_("bundle list at '%s' has no mode"), uri);
|
|
result = 1;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static char *find_temp_filename(void)
|
|
{
|
|
int fd;
|
|
struct strbuf name = STRBUF_INIT;
|
|
/*
|
|
* Find a temporary filename that is available. This is briefly
|
|
* racy, but unlikely to collide.
|
|
*/
|
|
fd = odb_mkstemp(&name, "bundles/tmp_uri_XXXXXX");
|
|
if (fd < 0) {
|
|
warning(_("failed to create temporary file"));
|
|
return NULL;
|
|
}
|
|
|
|
close(fd);
|
|
unlink(name.buf);
|
|
return strbuf_detach(&name, NULL);
|
|
}
|
|
|
|
static int download_https_uri_to_file(const char *file, const char *uri)
|
|
{
|
|
int result = 0;
|
|
struct child_process cp = CHILD_PROCESS_INIT;
|
|
FILE *child_in = NULL, *child_out = NULL;
|
|
struct strbuf line = STRBUF_INIT;
|
|
int found_get = 0;
|
|
|
|
strvec_pushl(&cp.args, "git-remote-https", uri, NULL);
|
|
cp.in = -1;
|
|
cp.out = -1;
|
|
|
|
if (start_command(&cp))
|
|
return 1;
|
|
|
|
child_in = fdopen(cp.in, "w");
|
|
if (!child_in) {
|
|
result = 1;
|
|
goto cleanup;
|
|
}
|
|
|
|
child_out = fdopen(cp.out, "r");
|
|
if (!child_out) {
|
|
result = 1;
|
|
goto cleanup;
|
|
}
|
|
|
|
fprintf(child_in, "capabilities\n");
|
|
fflush(child_in);
|
|
|
|
while (!strbuf_getline(&line, child_out)) {
|
|
if (!line.len)
|
|
break;
|
|
if (!strcmp(line.buf, "get"))
|
|
found_get = 1;
|
|
}
|
|
strbuf_release(&line);
|
|
|
|
if (!found_get) {
|
|
result = error(_("insufficient capabilities"));
|
|
goto cleanup;
|
|
}
|
|
|
|
fprintf(child_in, "get %s %s\n\n", uri, file);
|
|
|
|
cleanup:
|
|
if (child_in)
|
|
fclose(child_in);
|
|
if (finish_command(&cp))
|
|
return 1;
|
|
if (child_out)
|
|
fclose(child_out);
|
|
return result;
|
|
}
|
|
|
|
static int copy_uri_to_file(const char *filename, const char *uri)
|
|
{
|
|
const char *out;
|
|
|
|
if (starts_with(uri, "https:") ||
|
|
starts_with(uri, "http:"))
|
|
return download_https_uri_to_file(filename, uri);
|
|
|
|
if (skip_prefix(uri, "file://", &out))
|
|
uri = out;
|
|
|
|
/* Copy as a file */
|
|
return copy_file(filename, uri, 0);
|
|
}
|
|
|
|
static int unbundle_from_file(struct repository *r, const char *file)
|
|
{
|
|
int result = 0;
|
|
int bundle_fd;
|
|
struct bundle_header header = BUNDLE_HEADER_INIT;
|
|
struct string_list_item *refname;
|
|
struct strbuf bundle_ref = STRBUF_INIT;
|
|
size_t bundle_prefix_len;
|
|
|
|
if ((bundle_fd = read_bundle_header(file, &header)) < 0)
|
|
return 1;
|
|
|
|
if ((result = unbundle(r, &header, bundle_fd, NULL)))
|
|
return 1;
|
|
|
|
/*
|
|
* Convert all refs/heads/ from the bundle into refs/bundles/
|
|
* in the local repository.
|
|
*/
|
|
strbuf_addstr(&bundle_ref, "refs/bundles/");
|
|
bundle_prefix_len = bundle_ref.len;
|
|
|
|
for_each_string_list_item(refname, &header.references) {
|
|
struct object_id *oid = refname->util;
|
|
struct object_id old_oid;
|
|
const char *branch_name;
|
|
int has_old;
|
|
|
|
if (!skip_prefix(refname->string, "refs/heads/", &branch_name))
|
|
continue;
|
|
|
|
strbuf_setlen(&bundle_ref, bundle_prefix_len);
|
|
strbuf_addstr(&bundle_ref, branch_name);
|
|
|
|
has_old = !read_ref(bundle_ref.buf, &old_oid);
|
|
update_ref("fetched bundle", bundle_ref.buf, oid,
|
|
has_old ? &old_oid : NULL,
|
|
REF_SKIP_OID_VERIFICATION,
|
|
UPDATE_REFS_MSG_ON_ERR);
|
|
}
|
|
|
|
bundle_header_release(&header);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* This limits the recursion on fetch_bundle_uri_internal() when following
|
|
* bundle lists.
|
|
*/
|
|
static int max_bundle_uri_depth = 4;
|
|
|
|
static int fetch_bundle_uri_internal(struct repository *r,
|
|
const char *uri,
|
|
int depth)
|
|
{
|
|
int result = 0;
|
|
char *filename;
|
|
|
|
if (depth >= max_bundle_uri_depth) {
|
|
warning(_("exceeded bundle URI recursion limit (%d)"),
|
|
max_bundle_uri_depth);
|
|
return -1;
|
|
}
|
|
|
|
if (!(filename = find_temp_filename())) {
|
|
result = -1;
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((result = copy_uri_to_file(filename, uri))) {
|
|
warning(_("failed to download bundle from URI '%s'"), uri);
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((result = !is_bundle(filename, 0))) {
|
|
warning(_("file at URI '%s' is not a bundle"), uri);
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((result = unbundle_from_file(r, filename))) {
|
|
warning(_("failed to unbundle bundle from URI '%s'"), uri);
|
|
goto cleanup;
|
|
}
|
|
|
|
cleanup:
|
|
if (filename)
|
|
unlink(filename);
|
|
free(filename);
|
|
return result;
|
|
}
|
|
|
|
int fetch_bundle_uri(struct repository *r, const char *uri)
|
|
{
|
|
return fetch_bundle_uri_internal(r, uri, 0);
|
|
}
|
|
|
|
/**
|
|
* General API for {transport,connect}.c etc.
|
|
*/
|
|
int bundle_uri_parse_line(struct bundle_list *list, const char *line)
|
|
{
|
|
int result;
|
|
const char *equals;
|
|
struct strbuf key = STRBUF_INIT;
|
|
|
|
if (!strlen(line))
|
|
return error(_("bundle-uri: got an empty line"));
|
|
|
|
equals = strchr(line, '=');
|
|
|
|
if (!equals)
|
|
return error(_("bundle-uri: line is not of the form 'key=value'"));
|
|
if (line == equals || !*(equals + 1))
|
|
return error(_("bundle-uri: line has empty key or value"));
|
|
|
|
strbuf_add(&key, line, equals - line);
|
|
result = bundle_list_update(key.buf, equals + 1, list);
|
|
strbuf_release(&key);
|
|
|
|
return result;
|
|
}
|