{"record":{"id":"46771fd832746e98","repo":"can1357/oh-my-pi","slug":"gnu-multi-volume-tar-members-are-not-supported","errorCode":null,"errorMessage":"GNU multi-volume tar members are not supported","messagePattern":"GNU multi-volume tar members are not supported","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/tar.ts","lineNumber":486,"sourceCode":"\t\t\toffset += dataBlocks;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"N\") {\n\t\t\tapplyOldGnuNameRecords(data, entries, pendingLinks, limits);\n\t\t\toffset += dataBlocks;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"x\" || typeFlag === \"X\") {\n\t\t\tlocalPax = parsePaxRecords(data, limits);\n\t\t\toffset += dataBlocks;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"g\") {\n\t\t\tapplyGlobalPax(globalPax, parsePaxRecords(data, limits));\n\t\t\toffset += dataBlocks;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeFlag === \"M\") throw new ArchiveError(\"GNU multi-volume tar members are not supported\");\n\n\t\tif (longName !== undefined) name = longName;\n\t\tif (longLink !== undefined) linkName = longLink;\n\t\tconst paxPath = paxAttribute(globalPax, localPax, \"path\");\n\t\tif (paxPath !== undefined) name = paxPath;\n\t\tconst paxLinkPath = paxAttribute(globalPax, localPax, \"linkpath\");\n\t\tif (paxLinkPath !== undefined) linkName = paxLinkPath;\n\t\tconst paxSize = paxAttribute(globalPax, localPax, \"size\");\n\t\tif (paxSize !== undefined) size = parsePaxSize(paxSize, \"member size\");\n\t\tconst paxSparseName = paxAttribute(globalPax, localPax, \"GNU.sparse.name\");\n\t\tif (paxSparseName !== undefined) name = paxSparseName;\n\t\tlet displaySize = size;\n\t\tconst paxSparseRealSize = paxAttribute(globalPax, localPax, \"GNU.sparse.realsize\");\n\t\tif (paxSparseRealSize !== undefined) displaySize = parsePaxSize(paxSparseRealSize, \"sparse real size\");\n\t\tconst sparse = typeFlag === \"S\" || paxDeclaresSparse(globalPax, localPax);\n\t\tif (typeFlag === \"S\" && buffer[headerOffset + GNU_SPARSE_ISEXTENDED_OFFSET] === 1) {\n\t\t\tlet extended = true;\n\t\t\twhile (extended) {","sourceCodeStart":468,"sourceCodeEnd":504,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/tar.ts#L468-L504","documentation":"Thrown when a tar header has GNU type flag 'M', which marks a continuation member from a GNU multi-volume (split) archive. This library indexes single-volume archives only and does not implement splicing member data across volumes, so it rejects the archive explicitly instead of producing a member with missing bytes. The archive itself may be perfectly valid multi-volume tar — it is just out of scope for this parser.","triggerScenarios":"Calling readTarEntriesFromBuffer/readTar on an archive created with `tar -M -L <size> -F` (multi-volume) or GNU split backups, at the first header belonging to volume 2+.","commonSituations":"Old GNU backup scripts (level-0 dumps with -M), tapes split across media converted to files, or someone splitting a tar with an OS-aware splitter that emits proper multi-volume headers rather than raw byte cuts.","solutions":["Re-create the archive without multi-volume mode: `tar -cf out.tar dir/` (split afterwards with plain file splitting if size-limited).","Concatenate volumes only works for raw-split files, not true -M archives; use GNU tar to extract fully first (`tar -xMf vol1 -M -f vol2`) and repack as a single tar.","Use a tar library that supports multi-volume archives for the read side.","If you control the producer, switch to per-chunk archives (chunk1.tar, chunk2.tar) and parse each independently."],"exampleFix":"// before\ntar -M -L 1G -F split-script -cf backup.tar data/\n// after: single archive, split as plain bytes if needed\ntar -cf backup.tar data/\nsplit -b 1G backup.tar backup.tar.part-","handlingStrategy":"fallback","validationCode":"// peek first volume headers for GNU multi-volume flag 'M' before parsing\nfunction usesMultiVolume(bytes) {\n  for (let off = 0; off + 512 <= bytes.byteLength; off += 512) {\n    if (bytes.every.call(bytes.subarray(off, off + 512), (b) => b === 0)) return false;\n    if (String.fromCharCode(bytes[off + 156]) === 'M') return true;\n    const size = parseInt(bytes.subarray(off + 124, off + 136).toString().replace(/[^0-7]/g, ''), 8) || 0;\n    off += Math.ceil(size / 512) * 512; // skip data blocks\n  }\n  return false;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return readTarEntriesFromBuffer(bytes, { limits });\n} catch (err) {\n  if (err instanceof ArchiveError && /multi-volume/.test(err.message)) {\n    const merged = await mergeMultiVolumeWithGnuTar(volumePaths); // tar -xM then repack\n    return readTarEntriesFromBuffer(merged, { limits });\n  }\n  throw err;\n}","preventionTips":["Create archives without `tar -M`; split plain files afterwards instead.","When receiving tape-style backups, ask for a single-volume repack upstream.","Detect GNU multi-volume input early and route it to GNU tar rather than this parser.","Document multi-volume as unsupported for consumers of your pipeline."],"tags":["tar","gnu","multi-volume","unsupported-feature"],"backgroundTag":"unsupported-tar-feature","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}