| git.druid.rocks | index | druid520 | mp | src/ | merge.c |
src/merge.c
#define _POSIX_C_SOURCE 200809L
#define _NETBSD_SOURCE 1
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "mpx.h"
/* creates every ancestor directory of path (path itself is a file, not
* a directory to create) -- same "walk each '/' boundary" approach as a
* shell's own mkdir -p, tolerating EEXIST (the common case: most of a
* package's directory tree already exists from an earlier file in the
* same manifest, or from another package entirely). anything else
* (ENOTDIR -- a path component already exists as a plain file/symlink,
* genuine corruption) is fatal: silently continuing would merge this
* file somewhere other than where its manifest says it belongs. */
static void
mkdirp(const char* path)
{
char buf[MAXPATH];
size_t n = strlen(path);
size_t i;
if(n >= sizeof(buf))
{
die("path too long");
return;
}
memcpy(buf, path, n + 1);
for(i = 1; i < n; i = i + 1)
{
if(buf[i] == '/')
{
buf[i] = '\0';
if(mkdir(buf, 0755) != 0 && errno != EEXIST)
{
die("failed creating destination directory");
return;
}
buf[i] = '/';
}
}
}
/* atomically replaces dst with a symlink to whatever src points at:
* built as a fresh symlink at dst+".mp-merge-tmp" then rename()d over
* dst, same reasoning as the regular-file case in merge_one below --
* dst is never briefly missing partway through.
*
* an ABSOLUTE target rooted at srcprefix is rewritten to the same
* relative path under dstprefix instead of copied verbatim -- a port's
* own install phase routinely bakes its OWN $MP_PREFIX into a symlink
* it creates (musl's own install.sh -D -l does exactly this for
* ld-musl-*.so.1 -> libc.so), and src-prefix is a throwaway build
* location for both mp.sysroot and "mp reinstall --inplace": copying
* that target as-is leaves a symlink pointing at a path that is about
* to stop existing the moment the ephemeral/sysroot build is cleaned
* up. hit for real during this feature's own use: an --inplace musl
* rebuild left /lib/ld-musl-x86_64.so.1 pointing at
* /usr/mp/.inplace/musl/root/lib/libc.so, breaking every dynamically
* linked binary on the live root (including the shell) the moment that
* scratch directory was removed. a target that ISN'T rooted at
* srcprefix (relative, or absolute but pointing somewhere else
* entirely) is left exactly as-is. */
static void
merge_symlink(const char* srcprefix, const char* dstprefix, const char* src, const char* dst)
{
char target[MAXPATH];
char final[MAXPATH];
char tmp[MAXPATH];
size_t off = 0;
size_t srclen = strlen(srcprefix);
ssize_t tn = readlink(src, target, sizeof(target) - 1);
if(tn < 0)
{
die("failed reading source symlink");
return;
}
target[tn] = '\0';
if(strncmp(target, srcprefix, srclen) == 0
&& (target[srclen] == '/' || target[srclen] == '\0'))
{
size_t foff = 0;
if(bufcat(final, sizeof(final), &foff, dstprefix) == 0
|| bufcat(final, sizeof(final), &foff, target + srclen) == 0)
{
die("path too long");
return;
}
}
else
{
if(strlen(target) >= sizeof(final))
{
die("path too long");
return;
}
memcpy(final, target, (size_t)tn + 1);
}
if(bufcat(tmp, sizeof(tmp), &off, dst) == 0
|| bufcat(tmp, sizeof(tmp), &off, ".mp-merge-tmp") == 0)
{
die("path too long");
return;
}
unlink(tmp);
if(symlink(final, tmp) != 0)
{
die("failed creating replacement symlink");
return;
}
if(rename(tmp, dst) != 0)
{
die("failed replacing destination symlink");
return;
}
}
/* atomically replaces dst with src's content and permission bits: full
* content is streamed straight from src's fd to a temp file in dst's
* own directory (never loaded whole into memory the way readfile/
* writefile's MAXFILE cap would require -- a real installed binary,
* unlike a port script or config file, routinely exceeds 1MB), then
* rename()d over dst so dst is never briefly truncated or missing. */
static void
merge_regular(const char* src, const char* dst, mode_t mode)
{
static char buf[65536];
char tmp[MAXPATH];
size_t off = 0;
int sfd;
int dfd;
ssize_t n;
if(bufcat(tmp, sizeof(tmp), &off, dst) == 0
|| bufcat(tmp, sizeof(tmp), &off, ".mp-merge-tmp") == 0)
{
die("path too long");
return;
}
sfd = open(src, O_RDONLY);
if(sfd < 0)
{
die("failed opening source file");
return;
}
unlink(tmp);
dfd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if(dfd < 0)
{
/* die() exits the whole process in practice, so this close()
* never actually runs -- kept anyway (and checked by every
* return below that follows an fd open) so this file is
* correct plain C89 even without relying on that: no
* noreturn hint on die() (a GNU/Clang extension mpx doesn't
* use, see mpx.h's own comment) means the compiler must treat
* every "return;" here as a real path or the leak. */
close(sfd);
die("failed creating replacement file");
return;
}
n = read(sfd, buf, sizeof(buf));
while(n > 0)
{
ssize_t off2 = 0;
while(off2 < n)
{
ssize_t w = write(dfd, buf + off2, (size_t)(n - off2));
if(w <= 0)
{
close(sfd);
close(dfd);
die("failed writing replacement file");
return;
}
off2 = off2 + w;
}
n = read(sfd, buf, sizeof(buf));
}
if(n < 0 || fchmod(dfd, mode & 07777) != 0)
{
close(sfd);
close(dfd);
die("failed finishing replacement file");
return;
}
close(sfd);
close(dfd);
if(rename(tmp, dst) != 0)
{
die("failed replacing destination file");
return;
}
}
/* one manifest entry: rel is INSTPREFIX-relative, exactly as written by
* mplib::db::write_manifest (and read back by read_lines) -- no leading
* slash, no trailing newline. */
static void
merge_one(const char* srcprefix, const char* dstprefix, const char* rel)
{
char src[MAXPATH];
char dst[MAXPATH];
struct stat st;
if(!pathfits(srcprefix, rel) || !pathfits(dstprefix, rel)
|| pathjoin(src, sizeof(src), srcprefix, rel) == 0
|| pathjoin(dst, sizeof(dst), dstprefix, rel) == 0)
{
die("path too long");
return;
}
if(lstat(src, &st) != 0)
{
die("manifest lists a file the source tree no longer has");
return;
}
mkdirp(dst);
if(S_ISLNK(st.st_mode))
{
merge_symlink(srcprefix, dstprefix, src, dst);
}
else if(S_ISREG(st.st_mode))
{
merge_regular(src, dst, st.st_mode);
}
else if(S_ISDIR(st.st_mode))
{
/* fstree.c's own "path\tdir" snapshot marker (added this
* session, tracking a directory's own existence -- e.g. an
* otherwise-empty directory a package's install: phase
* creates) means a manifest CAN legitimately list a directory
* now, not just regular files and symlinks -- this comment's
* own claim otherwise was true when merge.c was first written,
* not anymore. mkdirp above only ever creates rel's ANCESTORS
* (by design, see its own comment: "path itself is a file, not
* a directory"), so the leaf itself still needs creating here.
* EEXIST tolerated same as mkdirp's own: a directory shared
* with another already-merged package (or one this same
* package already created via an earlier manifest entry) is
* the common case, not corruption. */
if(mkdir(dst, st.st_mode) != 0 && errno != EEXIST)
{
die("failed creating destination directory");
return;
}
}
else
{
/* walktree (the only writer of any manifest) never records
* anything but regular files, symlinks, and (as of the fix
* above) directories -- anything else here means the manifest
* and the tree it describes have already diverged. */
die("manifest entry is neither a regular file, symlink, nor directory");
return;
}
}
/* mpx merge <src-prefix> <dst-prefix> <manifest-file>
*
* folds one package's (or, called once per line-group by a caller that
* concatenates several, a whole sysroot's) installed files from a
* built-elsewhere tree onto the live one: every path the manifest lists
* is copied from src-prefix into dst-prefix, overwriting whatever was
* there, atomically per file/symlink (see merge_regular/merge_symlink).
* never deletes anything -- a path dst-prefix has that the manifest
* doesn't mention is simply left alone, on purpose (see mp.sysroot's
* own comment for why: this is an overlay, not a sync).
*
* this is the primitive mp.sysroot and "mp reinstall --inplace" both
* build on to avoid the del.sh-then-add.sh window a normal reinstall
* goes through -- the new tree is fully built (into a sysroot, or an
* ephemeral stand-in for one) before a single live file changes, and
* every file that does change is replaced in one rename(), never
* deleted-then-recreated. */
void
cmd_merge(int argc, char** argv)
{
static char manifest[MAXFILE];
long n;
long start;
long i;
if(argc != 5)
{
badusage(argv[0], "merge <src-prefix> <dst-prefix> <manifest-file>");
return;
}
n = readfile(argv[4], manifest, (long)sizeof(manifest));
if(n == -2)
{
die("manifest too large to merge (increase MAXFILE)");
return;
}
if(n < 0)
{
die("failed reading manifest");
return;
}
start = 0;
for(i = 0; i < n; i = i + 1)
{
if(manifest[i] == '\n')
{
if(i > start)
{
char rel[MAXPATH];
long len = i - start;
if((size_t)len >= sizeof(rel))
{
die("manifest entry too long");
return;
}
memcpy(rel, manifest + start, (size_t)len);
rel[len] = '\0';
merge_one(argv[2], argv[3], rel);
}
start = i + 1;
}
}
}