/*******************************************************************************
Copyright(C) Jonas 'Sortie' Termansen 2013.
This file is part of Tix.
Tix is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.
Tix is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
You should have received a copy of the GNU General Public License along with
Tix. If not, see .
tix-object-insert.cpp
Inserts files into a tix object database.
*******************************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int create_and_open_directory_at(int dirfd, const char* path, mode_t mode)
{
int ret = openat(dirfd, path, O_RDONLY | O_DIRECTORY);
if ( ret < 0 && errno == EEXIST )
{
if ( mkdirat(dirfd, path, mode) != 0 )
return -1;
return openat(dirfd, path, O_RDONLY | O_DIRECTORY);
}
return ret;
}
struct options
{
bool hash;
bool quiet;
bool link;
bool symlink;
};
bool insert_object(struct options* opts,
const char* file_path,
const char* tmp,
int database_fd, const char* database_path,
const char* digest,
int dir_fd, const char* dir_name)
{
const char* entry_name = digest + 2;
if ( linkat(database_fd, tmp, dir_fd, entry_name, 0) &&
errno != EEXIST )
{
error(0, errno, "`%s/%s' -> `%s/%s/%s'", database_path, dir_name,
database_path, dir_name, entry_name);
return false;
}
if ( opts->hash )
printf("%s\n", digest);
else if ( !opts->quiet )
printf("`%s' -> `%s/%s/%s'\n", file_path,
database_path, dir_name, entry_name);
return true;
}
bool insert_object(struct options* opts,
const char* file_path,
const char* tmp,
int database_fd, const char* database_path,
const char* digest)
{
char dir_name[3] = { digest[0], digest[1], '\0' };
int dir_fd = create_and_open_directory_at(database_fd, dir_name, 0777);
if ( dir_fd < 0 )
{
error(0, errno, "`%s/%s'", database_path, dir_name);
return false;
}
bool success = insert_object(opts, file_path, tmp, database_fd, database_path, digest, dir_fd, dir_name);
close(dir_fd);
return success;
}
bool copy_and_hash(FILE* fpin, const char* file_path,
FILE* fpout, const char* tmp,
int /*database_fd*/, const char* database_path,
char* digest)
{
struct sha256_ctx shactx;
sha256_init(&shactx);
uint8_t buffer[SHA256_DATA_SIZE];
while ( size_t num_bytes = fread(buffer, 1, sizeof(buffer), fpin) )
{
assert(num_bytes <= UINT_MAX);
sha256_update(&shactx, num_bytes, buffer);
if ( fwrite(buffer, 1, num_bytes, fpout) != num_bytes )
{
error(0, errno, "`%s/%s'", database_path, tmp);
return false;
}
}
if ( ferror(fpin) )
{
error(0, errno, "`%s'", file_path);
return false;
}
if ( fflush(fpout) != 0 )
{
error(0, errno, "`%s/%s'", database_path, tmp);
return false;
}
uint8_t binary_digest[SHA256_DIGEST_SIZE];
sha256_digest(&shactx, sizeof(binary_digest), binary_digest);
for ( size_t n = 0; n < SHA256_DIGEST_SIZE; n++ )
snprintf(digest + 2 * n, 3, "%02x", binary_digest[n]);
return true;
}
bool insert_object(struct options* opts,
FILE* fpin, const char* file_path,
FILE* fpout, const char* tmp,
int database_fd, const char* database_path)
{
char digest[2 * SHA256_DIGEST_SIZE + 1];
if ( !copy_and_hash(fpin, file_path, fpout, tmp, database_fd,
database_path, digest) )
return false;
return insert_object(opts, file_path, tmp, database_fd, database_path, digest);
}
// TODO: Preferably use O_TMPFILE to avoid naming the file until we have made a
// copy whose hash we know.
bool insert_object(struct options* opts,
FILE* fpin, const char* file_path,
int database_fd, const char* database_path)
{
char tmp[8 + 1 + sizeof(pid_t) * 3 + 1];
snprintf(tmp, sizeof(tmp), "incoming.%ji", (intmax_t) getpid());
int tmp_fd = openat(database_fd, tmp, O_CREAT | O_WRONLY | O_EXCL, 0444);
if ( tmp_fd < 0 )
{
error(0, errno, "`%s/%s'", database_path, tmp);
return false;
}
FILE* fpout = fdopen(tmp_fd, "w");
if ( !fpout )
{
error(0, errno, "fdopen(%i)", tmp_fd);
close(tmp_fd);
return false;
}
bool success = insert_object(opts, fpin, file_path, fpout, tmp, database_fd, database_path);
fclose(fpout);
unlinkat(database_fd, tmp, 0);
return success;
}
bool insert_object(struct options* opts,
const char* file_path,
int database_fd, const char* database_path)
{
if ( !strcmp(file_path, "-") )
return insert_object(opts, stdin, file_path, database_fd, database_path);
FILE* fpin = fopen(file_path, "r");
if ( !fpin )
error(1, errno, "`%s'", file_path);
bool success = insert_object(opts, fpin, file_path, database_fd, database_path);
fclose(fpin);
return success;
}
void help(FILE* fp, const char* argv0)
{
fprintf(fp, "Usage: %s [OPTION]... [-l | -s] --database DATABASE FILE...\n", argv0);
fprintf(fp, "Inserts files into a tix object database.\n");
}
void usage(FILE* fp, const char* argv0)
{
help(fp, argv0);
}
void version(FILE* fp, const char* argv0)
{
help(fp, argv0);
}
int main(int argc, char* argv[])
{
const char* argv0 = argv[0];
const char* database_path = NULL;
bool hash = false;
bool quiet = false;
bool do_link = false;
bool do_symlink = false;
for ( int i = 0; i < argc; i++ )
{
const char* arg = argv[i];
if ( arg[0] != '-' )
continue;
argv[i] = NULL;
if ( !strcmp(arg, "--") )
break;
if ( arg[1] != '-' )
{
while ( char c = *++arg ) switch ( c )
{
case 'h': hash = true;
case 'l': do_link = true;
case 'q': quiet = true;
case 's': do_link = true;
default:
fprintf(stderr, "%s: unknown option -- '%c'\n", argv0, c);
usage(stderr, argv0);
exit(1);
}
}
else if ( !strcmp(arg, "--help") )
help(stdout, argv0), exit(0);
else if ( !strcmp(arg, "--usage") )
usage(stdout, argv0), exit(0);
else if ( !strcmp(arg, "--version") )
version(stdout, argv0), exit(0);
else if ( !strcmp(arg, "--database") )
{
if ( i + 1 == argc )
error(1, 0, "`--database' expected argument");
database_path = argv[++i], argv[i] = NULL;
}
else if ( !strcmp(arg, "--hash") )
hash = true;
else if ( !strcmp(arg, "--link") )
do_link = true;
else if ( !strcmp(arg, "--quiet") )
quiet = true;
else if ( !strcmp(arg, "--symlink") )
do_symlink = true;
else
{
fprintf(stderr, "%s: unknown option: `%s'\n", argv0, arg);
usage(stderr, argv0);
exit(1);
}
}
if ( argc == 1 )
usage(stdout, argv0), exit(0);
if ( do_link && do_symlink )
{
error(0, 0, "error: the -l and -s options are mutually exclusive");
usage(stdout, argv0);
exit(1);
}
if ( !database_path )
{
error(1, 0, "no `--database' option given, don't know what database");
usage(stdout, argv0);
exit(1);
}
int database_fd = open(database_path, O_RDONLY | O_DIRECTORY);
if ( database_fd < 0 )
error(1, errno, "`%s'", database_path);
for ( int i = 1; i < argc; i++ )
{
const char* file_path = argv[i];
if ( !file_path )
continue;
FILE* fpin = fopen(file_path, "r");
if ( !fpin )
error(1, errno, "`%s'", file_path);
char tmp[8 + 1 + sizeof(pid_t) * 3 + 1];
snprintf(tmp, sizeof(tmp), "incoming.%ju", (uintmax_t) getpid());
int tmp_fd = openat(database_fd, tmp, O_CREAT | O_WRONLY | O_EXCL, 0444);
if ( tmp_fd < 0 )
error(1, errno, "`%s/%s'", database_path, tmp);
FILE* fpout = fdopen(tmp_fd, "w");
struct sha256_ctx shactx;
sha256_init(&shactx);
uint8_t buffer[SHA256_DATA_SIZE];
while ( size_t num_bytes = fread(buffer, 1, sizeof(buffer), fpin) )
{
assert(num_bytes <= UINT_MAX);
sha256_update(&shactx, num_bytes, buffer);
if ( fwrite(buffer, 1, num_bytes, fpout) != num_bytes )
{
unlinkat(database_fd, tmp, 0);
error(1, errno, "`%s/%s'", database_path, tmp);
}
}
if ( ferror(fpin) )
{
unlinkat(database_fd, tmp, 0);
error(1, errno, "`%s'", file_path);
}
fclose(fpin);
if ( fflush(fpout) != 0 )
{
unlinkat(database_fd, tmp, 0);
error(1, errno, "`%s/%s'", database_path, tmp);
}
fclose(fpout);
uint8_t binary_digest[SHA256_DIGEST_SIZE];
sha256_digest(&shactx, sizeof(binary_digest), binary_digest);
char digest[2 * SHA256_DIGEST_SIZE + 1];
for ( size_t n = 0; n < SHA256_DIGEST_SIZE; n++ )
snprintf(digest + 2 * n, 3, "%02x", binary_digest[n]);
char dir_name[3] = { digest[0], digest[1], '\0' };
if ( mkdirat(database_fd, dir_name, 0777) != 0 &&
errno != EEXIST )
{
unlinkat(database_fd, tmp, 0);
error(1, errno, "`%s/%s'", database_path, dir_name);
}
int dir_fd = openat(database_fd, dir_name, O_RDONLY | O_DIRECTORY);
if ( dir_fd < 0 )
{
unlinkat(database_fd, tmp, 0);
error(1, errno, "`%s/%s'", database_path, dir_name);
}
const char* entry_name = digest + 2;
if ( linkat(database_fd, tmp, dir_fd, entry_name, 0) &&
errno != EEXIST )
{
unlinkat(database_fd, tmp, 0);
error(1, errno, "`%s/%s' -> `%s/%s/%s'", database_path, dir_name,
database_path, dir_name, entry_name);
}
close(dir_fd);
unlinkat(database_fd, tmp, 0);
if ( hash )
printf("%s\n", digest);
else if ( !quiet )
printf("`%s' -> `%s/%s/%s'\n", file_path,
database_path, dir_name, entry_name);
if ( do_link || do_symlink )
{
if ( unlink(file_path) < 0 )
error(1, errno, "cannot unlink: `%s'", file_path);
size_t link_dest_length = strlen(database_path) + 1 + strlen(dir_name) + strlen(entry_name);
char* link_dest = (char*) malloc(sizeof(char) * (link_dest_length + 1));
stpcpy(stpcpy(stpcpy(stpcpy(stpcpy(link_dest, database_path), "/"), dir_name), "/"), entry_name);
if ( do_symlink )
{
if ( symlink(link_dest, file_path) < 0 )
error(1, errno, "cannot symlink `%s' to `%s'", file_path,
link_dest);
}
else
{
if ( link(link_dest, file_path) < 0 )
error(1, errno, "cannot link `%s' to `%s'", file_path,
link_dest);
}
free(link_dest);
}
}
close(database_fd);
return 0;
}