rpm: pure-C cpio unpacker + codec-aware decompress (works on systems without cpio/rpm/rpm2cpio/alien)

- Add pmm_cpio_unpack(): parses newc/070701, crc/070702 and odc/070707 archives
  in pure C (no external cpio), extracting paths under a stage dir.
- rpm install now detects the payload codec (gzip/zstd/xz/bz2/lzma) and feeds
  the decompressor's stdout through the built-in cpio unpacker, then sudo-copies
  the payload into /. No host cpio/rpm/rpm2cpio/alien required.
- Fix RHEL9/Fedora zstd rpms failing with "not in gzip format" (gzip was
  blindly assumed). A failed decompress now returns non-zero instead of faking
  a successful install.
这个提交包含在:
JGZYES
2026-09-03 22:34:59 +08:00
父节点 6aef0af546
当前提交 093b0e42f1
+187 -6
查看文件
@@ -28,6 +28,7 @@
#define PMM_MKDIR(p) _mkdir(p)
#else
#include <sys/stat.h>
#include <unistd.h> /* unlink/symlink (cpio unpacker) */
#define PMM_MKDIR(p) mkdir((p), 0755)
#endif
@@ -229,6 +230,158 @@ static int rpm_extract_payload(const char *rpm, const char *out) {
return 0;
}
/* Detect the compression codec of an RPM payload so we can pick the right
* decompressor without assuming gzip. Returns:
* 0=gzip 1=zstd 2=xz 3=bzip2 4=lzma, or -1 if unrecognized. */
static int rpm_payload_compress(const char *payload) {
FILE *f = fopen(payload, "rb");
if (!f) return -1;
unsigned char m[8];
size_t n = fread(m, 1, 8, f);
fclose(f);
if (n >= 2 && m[0] == 0x1f && m[1] == 0x8b) return 0; /* gzip */
if (n >= 4 && m[0] == 0x28 && m[1] == 0xb5 && m[2] == 0x2f && m[3] == 0xfd) return 1; /* zstd */
if (n >= 6 && m[0] == 0xfd && m[1] == 0x37 && m[2] == 0x7a && m[3] == 0x58 &&
m[4] == 0x5a && m[5] == 0x00) return 2; /* xz */
if (n >= 3 && m[0] == 0x42 && m[1] == 0x5a && m[2] == 0x68) return 3; /* bzip2 */
if (n >= 5 && m[0] == 0x5d && m[1] == 0x00 && m[2] == 0x00 && m[3] == 0x00 && m[4] == 0x80) return 4; /* lzma */
return -1;
}
/* ---------- pure-C cpio unpacker (no system cpio required) ----------
* Reads a cpio archive from FILE* `in` (typically a popen decompression pipe)
* and writes paths relative to `dest`. Handles newc (070701), newc-crc (070702)
* and old odc (070707). Archive names are always kept under dest ('.' and '/'
* are stripped; '..' names are skipped). Returns 0 on success. */
static unsigned c_hexn(const char *s, int n) {
unsigned v = 0; int i;
for (i = 0; i < n; i++) { char ch = s[i]; v <<= 4;
if (ch >= '0' && ch <= '9') v |= (unsigned)(ch - '0');
else if (ch >= 'a' && ch <= 'f') v |= (unsigned)(ch - 'a' + 10);
else if (ch >= 'A' && ch <= 'F') v |= (unsigned)(ch - 'A' + 10); }
return v;
}
/* Create every directory component under the (final) path. */
static void cpio_mkdirs(char *path) {
for (char *p = path + 1; *p; p++) {
if (*p == '/') { char c = *p; *p = '\0'; mkdir(path, 0755); *p = c; }
}
}
/* Skip exactly n bytes from a possibly non-seekable stream. */
static void cpio_skip(FILE *in, unsigned long n) {
char b[2048];
while (n) { unsigned long c = n > sizeof(b) ? sizeof(b) : n;
size_t g = fread(b, 1, c, in); if (!g) break; n -= g; }
}
int pmm_cpio_unpack(const char *dest, FILE *in) {
char name[8192];
int first = 1, is_odc = 0;
for (;;) {
/* read the 6-byte magic */
char hdr[110];
if (fread(hdr, 1, 6, in) != 6) break; /* EOF -> done (no trailer) */
if (!(memcmp(hdr,"070701",6)==0 || memcmp(hdr,"070702",6)==0 ||
memcmp(hdr,"070707",6)==0)) break; /* not a cpio magic -> done */
if (first) { is_odc = (memcmp(hdr,"070707",6)==0); first = 0; }
int hlen;
if (is_odc) { hlen = 76; if (fread(hdr+6,1,70,in)!=70) return -1; }
else { hlen = 110; if (fread(hdr+6,1,104,in)!=104) return -1; }
unsigned mode, data_size, namesize;
if (is_odc) {
mode = c_hexn(hdr+6, 6);
data_size = c_hexn(hdr+6+30, 6);
namesize = 0; /* odc name is NUL-terminated */
} else {
/* newc (070701) / crc (070702): fields are 8-hex-char ASCII words
* that follow the 6-byte magic directly:
* ino(6) mode(14) uid(22) gid(30) nlink(38) mtime(46) filesize(54)
* devmaj(62) devmin(70) rdevmaj(78) rdevmin(86) name(94) check(102)
* (offsets are relative to hdr; magic occupies hdr[0..5]). */
mode = c_hexn(hdr+14, 8);
data_size = c_hexn(hdr+54, 8);
namesize = c_hexn(hdr+94, 8);
}
if (is_odc) {
/* NUL-terminated name of unbounded length */
int pos = 0;
for (;;) {
int ch = fgetc(in);
if (ch == EOF) return -1;
if (pos < (int)sizeof(name)-1) name[pos++] = (char)ch;
if (ch == 0) break;
}
name[pos] = 0;
} else {
if (namesize == 0 || namesize >= sizeof(name)) namesize = (unsigned)sizeof(name)-1;
if (fread(name, 1, namesize, in) != namesize) return -1;
name[namesize] = 0;
/* align to 4 on the (header+name) boundary */
unsigned padh = (unsigned)(4 - (((unsigned)hlen + namesize) & 3)) & 3;
cpio_skip(in, padh);
}
if (name[0]==0 || strcmp(name,"TRAILER!!!")==0) break; /* end of archive */
/* make safe relative path */
const char *rel = name;
while (*rel=='/') rel++;
while (rel[0]=='.' && rel[1]=='/') rel += 2;
if (strncmp(rel,"../",3)==0 || strstr(rel,"/../") || strcmp(rel,"..")==0) {
cpio_skip(in, data_size); /* refuse .. traversal */
if (!is_odc) cpio_skip(in, (4-(data_size&3))&3);
continue;
}
char full[8400];
snprintf(full, sizeof(full), "%s/%s", dest, rel);
unsigned ft = mode & 0170000;
if (S_ISREG(ft)) {
cpio_mkdirs(full);
FILE *of = fopen(full, "wb");
if (of) {
char b[65536]; unsigned long left = data_size;
while (left) { unsigned long c = left > sizeof(b) ? sizeof(b) : left;
size_t g = fread(b,1,c,in); if (!g) break; fwrite(b,1,g,of); left -= g; }
fclose(of);
chmod(full, mode & 07777);
} else cpio_skip(in, data_size);
} else if (S_ISDIR(ft)) {
cpio_mkdirs(full); mkdir(full, mode & 07777);
} else if (S_ISLNK(ft)) {
char t[4096];
if (data_size < sizeof(t)-1) {
if (fread(t,1,data_size,in)!=data_size) return -1;
t[data_size]=0; cpio_mkdirs(full); unlink(full);
if (symlink(t, full) != 0) { /* best-effort symlink */ }
} else cpio_skip(in, data_size);
} else {
cpio_skip(in, data_size); /* device/fifo/unknown */
}
if (!is_odc) cpio_skip(in, (4-(data_size&3))&3); /* data->4 pad */
}
return 0;
}
/* Run `decompcmd` (e.g. "gzip -dc file") and unpack its cpio output into dest.
* Needs decompcmd to emit a cpio stream on stdout. Returns 0 on success. */
static int pmm_cpio_unpack_cmd(const char *dest, const char *decompcmd) {
FILE *p = popen(decompcmd, "r");
if (!p) return -1;
int rc = pmm_cpio_unpack(dest, p);
int prc = pclose(p);
if (rc != 0) return -1;
return (prc == 0) ? 0 : -1;
}
int install_file(const char *url, const char *name) {
char cache[1024], path[1200];
pmm_cache_dir(cache, sizeof(cache));
@@ -334,16 +487,44 @@ static int install_path(const char *path, const char *name) {
path, path, path);
}
} else if (has_suffix(bname, ".rpm") && os == OS_LINUX) {
/* Self-extract in C (no rpm/rpm2cpio): pull cpio payload, gunzip, cpio it into / */
char cache[1024], fdata[1300];
/* Self-extract in C (no rpm/rpm2cpio): pull cpio payload, decompress,
* unpack into a cache dir, then sudo-copy to /. The decompressor is
* chosen from the payload's real codec (NOT assumed gzip -- RHEL9+ and
* Fedora ship zstd, some srpms use xz/lzma). `set -e` + each stage
* chained with && means a failed decompress aborts and returns non-zero
* instead of pmm falsely reporting success. */
char cache[1024], fdata[1300], fstage[1300];
pmm_cache_dir(cache, sizeof(cache));
snprintf(fdata, sizeof(fdata), "%s/.pmm-rpm-payload", cache);
snprintf(fstage, sizeof(fstage), "%s/.pmm-rpm-stage", cache);
if (rpm_extract_payload(path, fdata) == 0) {
/* Decompress with pmm's own pure-C cpio unpacker so no system
* cpio/rpm/rpm2cpio/alien is required. We only need ONE of
* gzip/zstd/xz/bzip2/lzma on the host (virtually every Linux has at
* least one). The decompressor is chosen from the payload's real
* codec -- gzip is NOT assumed (RHEL9+/Fedora ship zstd; some srpms
* use xz/lzma). */
int ct = rpm_payload_compress(fdata);
const char *dc = (ct == 1) ? "zstd" :
(ct == 2) ? "xz" :
(ct == 3) ? "bzip2" :
(ct == 4) ? "lzma" : "gzip";
char dcmd[1500];
snprintf(dcmd, sizeof(dcmd), "%s -dc \"%s\"", dc, fdata);
snprintf(cmd, sizeof(cmd),
"if command -v gzip >/dev/null 2>&1; then gzip -dc \"%s\" | (cd / && cpio -idm --quiet 2>/dev/null); "
"elif command -v zstd >/dev/null 2>&1; then zstd -dc \"%s\" | (cd / && cpio -idm --quiet 2>/dev/null); "
"else echo 'pmm: need gzip/zstd+cpio for .rpm payload' 1>&2; exit 1; fi; rm -f \"%s\"",
fdata, fdata, fdata);
"sudo cp -a \"%s\"/. / && rm -rf \"%s\" \"%s\"",
fstage, fstage, fdata);
/* unpack into stage (no sudo needed; stage is user-writable) */
if (pmm_cpio_unpack_cmd(fstage, dcmd) != 0) {
fprintf(stderr, "pmm: rpm payload decompress/cpio failed (need %s)\n", dc);
return -1;
}
/* copy the unpacked payload into /, then clean up */
if (system(cmd) != 0) {
fprintf(stderr, "pmm: failed to copy rpm payload into /\n");
return -1;
}
(void)ct;
} else {
snprintf(cmd, sizeof(cmd),
"if command -v rpm >/dev/null 2>&1; then sudo rpm -Uvh \"%s\"; "