diff --git a/llvm/include/llvm/Object/Archive.h b/llvm/include/llvm/Object/Archive.h index c97018d3231d5..cdb7d32311e4a 100644 --- a/llvm/include/llvm/Object/Archive.h +++ b/llvm/include/llvm/Object/Archive.h @@ -158,6 +158,46 @@ class LLVM_ABI BigArchiveMemberHeader Expected isThin() const override { return false; } }; +// Define file member header of z/OS archive. +// The fixed part of the member header (in EBCDIC) is: +// struct ar_hdr { +// char ar_name[16]; /* space-padded member name */ +// char ar_date[12]; /* date (decimal) */ +// char ar_uid[6]; /* user id (decimal) */ +// char ar_gid[6]; /* group id (decimal) */ +// char ar_mode[8]; /* access mode (octal) */ +// char ar_size[10]; /* length in bytes (decimal) */ +// char ar_fmag[2]; /* contains backtick (X'79'), followed by new line +// (X'15') */ +// }; +class ZOSArchiveMemberHeader : public ArchiveMemberHeader { +public: + ZOSArchiveMemberHeader(Archive const *Parent, const char *RawHeaderPtr, + uint64_t Size, Error *Err); + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + // Converted EBCDIC to ASCII header string fields. + std::string RawMemberName; + std::string MemberName; + std::string LastModified; + std::string UID; + std::string GID; + std::string AccessMode; + + void setMemberHeaderStrings(Error *Err, uint64_t Size); + + Expected getRawName() const override; + Expected getName(uint64_t Size) const override; + StringRef getRawAccessMode() const override; + StringRef getRawLastModified() const override; + StringRef getRawUID() const override; + StringRef getRawGID() const override; + Expected getSize() const override; + Expected isThin() const override { return false; } +}; + class LLVM_ABI Archive : public Binary { virtual void anchor(); @@ -343,7 +383,16 @@ class LLVM_ABI Archive : public Binary { /// Size field is 10 decimal digits long static const uint64_t MaxMemberSize = 9999999999; - enum Kind { K_GNU, K_GNU64, K_BSD, K_DARWIN, K_DARWIN64, K_COFF, K_AIXBIG }; + enum Kind { + K_GNU, + K_GNU64, + K_BSD, + K_DARWIN, + K_DARWIN64, + K_COFF, + K_AIXBIG, + K_ZOS + }; Kind kind() const { return (Kind)Format; } bool isThin() const { return IsThin; } @@ -434,6 +483,18 @@ class BigArchive : public Archive { bool has64BitGlobalSymtab() { return Has64BitGlobalSymtab; } }; +class ZOSArchive : public Archive { +public: + // Fixed-Length header. + struct FixLenHdr { + char Magic[sizeof(ZOSArchiveMagic) - 1]; ///< ZOS archive magic string. + }; + + ZOSArchive(MemoryBufferRef Source, Error &Err); + +private: + std::string SymbolTableBuf; // __.SYMDEF strings converted to ASCII. +}; } // end namespace object } // end namespace llvm diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp index 17c926e621f36..d95f9103747aa 100644 --- a/llvm/lib/Object/Archive.cpp +++ b/llvm/lib/Object/Archive.cpp @@ -17,6 +17,7 @@ #include "llvm/Object/Binary.h" #include "llvm/Object/Error.h" #include "llvm/Support/Chrono.h" +#include "llvm/Support/ConvertEBCDIC.h" #include "llvm/Support/Endian.h" #include "llvm/Support/EndianStream.h" #include "llvm/Support/Error.h" @@ -104,7 +105,14 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent, *Err = createMemberHeaderParseError(this, RawHeaderPtr, Size); return; } - if (ArMemHdr->Terminator[0] != '`' || ArMemHdr->Terminator[1] != '\n') { + // '\x79\x15' is the EBCDIC equivalent of '`\n' for the z/OS archive + // terminator. + bool ValidTerminator = + Parent->kind() == Archive::K_ZOS + ? (ArMemHdr->Terminator[0] == '\x79' && + ArMemHdr->Terminator[1] == '\x15') + : (ArMemHdr->Terminator[0] == '`' && ArMemHdr->Terminator[1] == '\n'); + if (!ValidTerminator) { if (Err) { std::string Buf; raw_string_ostream OS(Buf); @@ -118,8 +126,9 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent, consumeError(NameOrErr.takeError()); uint64_t Offset = RawHeaderPtr - Parent->getData().data(); *Err = malformedError(Msg + "at offset " + Twine(Offset)); - } else + } else { *Err = malformedError(Msg + "for " + NameOrErr.get()); + } } return; } @@ -368,6 +377,114 @@ Expected BigArchiveMemberHeader::getSize() const { return *SizeOrErr + alignTo(*NameLenOrErr, 2); } +template +std::string ebcdicFieldToASCII(const char (&Field)[N]) { + SmallString<64> Dst; + StringRef Src = StringRef(Field, N); + ConverterEBCDIC::convertToUTF8(Src, Dst); + return Dst.str().rtrim(" ").str(); +} + +ZOSArchiveMemberHeader::ZOSArchiveMemberHeader(const Archive *Parent, + const char *RawHeaderPtr, + uint64_t Size, Error *Err) + : ArchiveMemberHeader(Parent, RawHeaderPtr, Size, Err) { + ErrorAsOutParameter ErrAsOutParam(Err); + setMemberHeaderStrings(Err, Size); +} + +Expected ZOSArchiveMemberHeader::getSize() const { + return getArchiveMemberDecField("size", ebcdicFieldToASCII(ArMemHdr->Size), + Parent, this); +} + +Expected ZOSArchiveMemberHeader::getRawName() const { + return StringRef(RawMemberName); +} + +Expected ZOSArchiveMemberHeader::getName(uint64_t /*Size*/) const { + return StringRef(MemberName); +} + +StringRef ZOSArchiveMemberHeader::getRawAccessMode() const { + return StringRef(AccessMode); +} + +StringRef ZOSArchiveMemberHeader::getRawLastModified() const { + return StringRef(LastModified); +} + +StringRef ZOSArchiveMemberHeader::getRawUID() const { return StringRef(UID); } + +StringRef ZOSArchiveMemberHeader::getRawGID() const { return StringRef(GID); } + +void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) { + uint64_t Offset = + reinterpret_cast(ArMemHdr) - Parent->getData().data(); + + // Set RawMemberName + RawMemberName = ebcdicFieldToASCII(ArMemHdr->Name); + if (RawMemberName.empty() || RawMemberName[0] == ' ') { + *Err = malformedError("name contains a leading space for archive member " + "header at offset " + + Twine(Offset)); + return; + } + + // Set MemberName. + if (StringRef(RawMemberName).starts_with("#1/")) { + Expected NameOrErr = ArchiveMemberHeader::getName(Size); + if (!NameOrErr) { + *Err = NameOrErr.takeError(); + return; + } + StringRef Name = NameOrErr.get(); + SmallString<64> ConvertedName; + ConverterEBCDIC::convertToUTF8(Name, ConvertedName); + MemberName = std::string(ConvertedName); + } else { + MemberName = RawMemberName; + } + + // LastModified + LastModified = ebcdicFieldToASCII(ArMemHdr->LastModified); + if (LastModified.empty()) { + *Err = + malformedError("LastModified field is empty or contains only spaces in " + "archive member header at offset " + + Twine(Offset)); + return; + } + + // UID + UID = ebcdicFieldToASCII(ArMemHdr->UID); + if (UID.empty()) { + *Err = malformedError("UID field is empty or contains only spaces in " + "archive member header at offset " + + Twine(Offset)); + return; + } + + // GID + GID = ebcdicFieldToASCII(ArMemHdr->GID); + if (GID.empty()) { + *Err = malformedError("GID field is empty or contains only spaces in " + "archive member header at offset " + + Twine(Offset)); + return; + } + + // AccessMode + AccessMode = ebcdicFieldToASCII(ArMemHdr->AccessMode); + if (AccessMode.empty()) { + *Err = + malformedError("AccessMode field is empty or contains only spaces in " + "archive member header at offset " + + Twine(Offset)); + return; + } +} + Expected BigArchiveMemberHeader::getRawNameSize() const { return getArchiveMemberDecField( "NameLen", getFieldRawString(ArMemHdr->NameLen), Parent, this); @@ -668,6 +785,8 @@ Expected> Archive::create(MemoryBufferRef Source) { if (Buffer.starts_with(BigArchiveMagic)) Ret = std::make_unique(Source, Err); + else if (Buffer.starts_with(ZOSArchiveMagic)) + Ret = std::make_unique(Source, Err); else Ret = std::make_unique(Source, Err); @@ -680,6 +799,10 @@ std::unique_ptr Archive::createArchiveMemberHeader(const char *RawHeaderPtr, uint64_t Size, Error *Err) const { ErrorAsOutParameter ErrAsOutParam(Err); + + if (kind() == K_ZOS) + return std::make_unique(this, RawHeaderPtr, Size, + Err); if (kind() != K_AIXBIG) return std::make_unique(this, RawHeaderPtr, Size, Err); return std::make_unique(this, RawHeaderPtr, Size, @@ -714,6 +837,10 @@ Archive::Archive(MemoryBufferRef Source, Error &Err) Format = K_AIXBIG; IsThin = false; return; + } else if (Buffer.starts_with(ZOSArchiveMagic)) { + Format = K_ZOS; + IsThin = false; + return; } else { Err = make_error("file too small to be an archive", object_error::invalid_file_type); @@ -971,6 +1098,8 @@ object::Archive::Kind Archive::getDefaultKindForTriple(const Triple &T) { return object::Archive::K_AIXBIG; if (T.isOSWindows()) return object::Archive::K_COFF; + if (T.isOSzOS()) + return object::Archive::K_ZOS; return object::Archive::K_GNU; } @@ -1042,6 +1171,12 @@ Expected Archive::Symbol::getMember() const { // the archive of the member that defines the symbol. Which is what // is needed here. Offset = read64le(Offsets + SymbolIndex * 16 + 8); + } else if (Parent->kind() == K_ZOS) { + // Each entry in the offset array is 8 bytes long: + // A 4-byte offset followed by 4 bytes of coded attributes. + // We multiply the SymbolIndex by 8 to reach the correct entry, + // and read the first 4 bytes (the offset). + Offset = read32be(Offsets + SymbolIndex * 8); } else { // Skip offsets. uint32_t MemberCount = read32le(Buf); @@ -1171,6 +1306,15 @@ Archive::symbol_iterator Archive::symbol_begin() const { buf += ran_strx; } else if (kind() == K_AIXBIG) { buf = getStringTable().begin(); + } else if (kind() == K_ZOS) { + // The contents of the z/OS symbol table member are: + // 1. The number of symbols, NS (4-byte integer). + // 2. NS pairs of 4-byte integers (offset and attributes). Length is NS*8 + // bytes. + // 3. NS null terminated strings of corresponding symbol names. + // Here we skip parts 1 and 2 to reach the start of the string table. + uint32_t SymbolCount = read32be(buf); + buf += sizeof(uint32_t) + (SymbolCount * (sizeof(uint64_t))); } else { uint32_t member_count = 0; uint32_t symbol_count = 0; @@ -1244,6 +1388,8 @@ uint32_t Archive::getNumberOfSymbols() const { return read32le(buf) / 8; if (kind() == K_DARWIN64) return read64le(buf) / 16; + if (kind() == K_ZOS) + return read32be(buf); uint32_t member_count = 0; member_count = read32le(buf); buf += 4 + (member_count * 4); // Skip offsets. @@ -1448,3 +1594,75 @@ BigArchive::BigArchive(MemoryBufferRef Source, Error &Err) setFirstRegular(*I); Err = Error::success(); } + +ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err) + : Archive(Source, Err) { + ErrorAsOutParameter ErrAsOutParam(&Err); + + // Get the special members. + child_iterator I = child_begin(Err, false); + if (Err) + return; + child_iterator E = child_end(); + + // See if this is a valid empty archive and if so return. + if (I == E) { + Err = Error::success(); + return; + } + const Child *C = &*I; + + Expected NameOrErr = C->getRawName(); + if (!NameOrErr) { + Err = NameOrErr.takeError(); + return; + } + StringRef Name = NameOrErr.get(); + + if (Name == "__.SYMDEF") { + // Copy symbol table converting embedded EBCDIC names to ASCII. + // getBuffer() cannot fail here because the Child constructor and + // getNext() already validate that the member's size fits within + // the archive. + StringRef EbcdicSymbolTable = cantFail(C->getBuffer()); + if (EbcdicSymbolTable.size() < sizeof(uint32_t)) { + Err = malformedError( + "z/OS archive symbol table is too small to read the symbol count, " + "symbol table size is " + + Twine(EbcdicSymbolTable.size())); + return; + } + uint64_t EbcdicSymbolCount = read32be(EbcdicSymbolTable.data()); + uint64_t OffsetToEbcdicNames = + sizeof(uint32_t) + (EbcdicSymbolCount * (sizeof(uint64_t))); + if (OffsetToEbcdicNames > EbcdicSymbolTable.size()) { + Err = malformedError("z/OS archive symbol table names offset " + + Twine(OffsetToEbcdicNames) + + " exceeds symbol table size " + + Twine(EbcdicSymbolTable.size())); + return; + } + uint64_t EbcdicNamesSize = EbcdicSymbolTable.size() - OffsetToEbcdicNames; + const char *EbcdicNamesPtr = EbcdicSymbolTable.data() + OffsetToEbcdicNames; + StringRef EbcdicNames(EbcdicNamesPtr, EbcdicNamesSize); + + SmallString<64> Dst; + ConverterEBCDIC::convertToUTF8(EbcdicNames, Dst); + SymbolTableBuf.append(EbcdicSymbolTable.data(), OffsetToEbcdicNames); + SymbolTableBuf.append(Dst.str()); + SymbolTable = StringRef(SymbolTableBuf.data(), SymbolTableBuf.size()); + + ++I; + if (Err) + return; + C = &*I; + + setFirstRegular(*C); + Err = Error::success(); + return; + } + + setFirstRegular(*C); + Err = Error::success(); + return; +} diff --git a/llvm/lib/Object/ArchiveWriter.cpp b/llvm/lib/Object/ArchiveWriter.cpp index 6d2bbca179836..4610fb4303274 100644 --- a/llvm/lib/Object/ArchiveWriter.cpp +++ b/llvm/lib/Object/ArchiveWriter.cpp @@ -192,6 +192,7 @@ static bool isBSDLike(object::Archive::Kind Kind) { case object::Archive::K_GNU64: case object::Archive::K_AIXBIG: case object::Archive::K_COFF: + case object::Archive::K_ZOS: return false; case object::Archive::K_BSD: case object::Archive::K_DARWIN: @@ -287,6 +288,7 @@ static bool is64BitKind(object::Archive::Kind Kind) { case object::Archive::K_BSD: case object::Archive::K_DARWIN: case object::Archive::K_COFF: + case object::Archive::K_ZOS: return false; case object::Archive::K_AIXBIG: case object::Archive::K_DARWIN64: @@ -517,6 +519,7 @@ getSymbolicFile(MemoryBufferRef Buf, LLVMContext &Context, case object::Archive::K_COFF: case object::Archive::K_DARWIN: case object::Archive::K_DARWIN64: + case object::Archive::K_ZOS: return ObjOrErr.takeError(); } } diff --git a/llvm/test/Object/Inputs/generate_zos_archive.py b/llvm/test/Object/Inputs/generate_zos_archive.py new file mode 100644 index 0000000000000..5489612f2633e --- /dev/null +++ b/llvm/test/Object/Inputs/generate_zos_archive.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +"""Generate z/OS archive files + +z/OS archives use EBCDIC encoding for headers, magic bytes, and symbol names. +This script generates archives in place to avoid reliance on canned binaries. + +Usage examples: + # Valid archive with one member and symbol table: + %python %S/Inputs/generate_zos_archive.py --output %t.a \ + --symtab "foo:0" --member foo.o:%S/Inputs/foo.o + + # Empty archive: + %python %S/Inputs/generate_zos_archive.py --output %t.a --empty + + # Malformed member header: bad terminator + %python %S/Inputs/generate_zos_archive.py --output %t.a \ + --member foo.o --bad-terminator + + # Malformed __.SYMDEF header: bad terminator + %python %S/Inputs/generate_zos_archive.py --output %t.a \ + --member foo.o --symtab foo:0 --malform-symtab-hdr bad-terminator + + # Member with explicit hex content: + %python %S/Inputs/generate_zos_archive.py --output %t.a \ + --member foo.o:hex:deadbeef +""" + +import argparse +import struct +import sys +import os + +# EBCDIC / ASCII conversion table. +# fmt: off +ASCII_TO_EBCDIC_TABLE = ( + 0x00,0x01,0x02,0x03,0x37,0x2D,0x2E,0x2F,0x16,0x05,0x15,0x0B,0x0C,0x0D,0x0E,0x0F, + 0x10,0x11,0x12,0x13,0x3C,0x3D,0x32,0x26,0x18,0x19,0x3F,0x27,0x1C,0x1D,0x1E,0x1F, + 0x40,0x5A,0x7F,0x7B,0x5B,0x6C,0x50,0x7D,0x4D,0x5D,0x5C,0x4E,0x6B,0x60,0x4B,0x61, + 0xF0,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0x7A,0x5E,0x4C,0x7E,0x6E,0x6F, + 0x7C,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6, + 0xD7,0xD8,0xD9,0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xAD,0xE0,0xBD,0x5F,0x6D, + 0x79,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x91,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xC0,0x4F,0xD0,0xA1,0x07, +) +# fmt: on + + +def ascii_to_ebcdic(s): + """Convert an ASCII string/bytes to EBCDIC (IBM-1047).""" + if isinstance(s, str): + s = s.encode("ascii") + return bytes(ASCII_TO_EBCDIC_TABLE[b] for b in s) + + +def ebcdic_pad(s, width, pad_char=" "): + """Convert ASCII string to EBCDIC, right-padded with EBCDIC spaces.""" + ascii_padded = s.ljust(width, pad_char) + return ascii_to_ebcdic(ascii_padded) + + +# z/OS archive magic: "!\n" in EBCDIC. +ZOS_MAGIC = b"\x5a\x4c\x81\x99\x83\x88\x6e\x15" + +# Terminator: "`\n" in EBCDIC. +ZOS_TERMINATOR = b"\x79\x15" + +# EBCDIC newline for padding. +EBCDIC_NEWLINE = b"\x15" + + +def make_member_header( + name, + modtime, + uid, + gid, + mode, + size, + bad_terminator=False, + empty_name=False, + empty_uid=False, + empty_gid=False, + empty_modtime=False, + empty_mode=False, +): + """Build a 60-byte z/OS archive member header. + + Fields (all EBCDIC, space-padded): + ar_name: 16 bytes + ar_date: 12 bytes + ar_uid: 6 bytes + ar_gid: 6 bytes + ar_mode: 8 bytes + ar_size: 10 bytes + ar_fmag: 2 bytes (terminator) + Total: 60 bytes + """ + # Handle long names. + long_name_ext = b"" + if len(name) > 16: + name_ebcdic = ascii_to_ebcdic(name) + ext_len = len(name_ebcdic) + display_name = "#1/%d" % ext_len + long_name_ext = name_ebcdic + # The size field includes the extended name length. + size = size + ext_len + else: + display_name = name + + if empty_name: + hdr = ebcdic_pad(" ", 16) + else: + hdr = ebcdic_pad(display_name, 16) + + if empty_modtime: + hdr += ebcdic_pad("", 12) + else: + hdr += ebcdic_pad(str(modtime), 12) + + if empty_uid: + hdr += ebcdic_pad("", 6) + else: + hdr += ebcdic_pad(str(uid), 6) + + if empty_gid: + hdr += ebcdic_pad("", 6) + else: + hdr += ebcdic_pad(str(gid), 6) + + if empty_mode: + hdr += ebcdic_pad("", 8) + else: + hdr += ebcdic_pad(str(mode), 8) + + hdr += ebcdic_pad(str(size), 10) + + if bad_terminator: + hdr += b"\x00\x00" + else: + hdr += ZOS_TERMINATOR + + assert len(hdr) == 60, f"Header is {len(hdr)} bytes, expected 60" + return hdr + long_name_ext + + +def make_symtab(symbols, member_offsets, truncated=False, bad_count=False): + """Build a __.SYMDEF symbol table body. + + symbols: list of (symbol_name_ascii, member_index, attributes) + member_offsets: list of offsets for each member (indexed by member_index) + + Format: + 4 bytes: number of symbols (big-endian) + For each symbol: 4 bytes offset + 4 bytes attributes (big-endian) + Null-terminated symbol names in EBCDIC + """ + num_syms = len(symbols) + if bad_count: + # Write a count that exceeds the buffer. + body = struct.pack(">I", 0xFFFFFFFF) + else: + body = struct.pack(">I", num_syms) + + if truncated: + # Return just the count, truncated before offset table. + return body[:2] + + for sym_name, mem_idx, attrs in symbols: + offset = member_offsets[mem_idx] + body += struct.pack(">II", offset, attrs) + + for sym_name, mem_idx, attrs in symbols: + body += ascii_to_ebcdic(sym_name) + b"\x00" + + return body + + +def parse_member_data(raw): + """Parse the data portion of a --member argument. + + Supports three forms: + /path/to/file - read file contents + hex: - decode hex bytes + - encode as raw ASCII bytes + """ + if os.path.isfile(raw): + with open(raw, "rb") as f: + return f.read() + if raw.startswith("hex:"): + return bytes.fromhex(raw[4:]) + return raw.encode("ascii") + + +# Valid malformation names for --malform-symtab-hdr, mapped to +# make_member_header keyword arguments. +_SYMTAB_HDR_MALFORMATIONS = { + "bad-terminator": "bad_terminator", + "empty-name": "empty_name", + "empty-uid": "empty_uid", + "empty-gid": "empty_gid", + "empty-modtime": "empty_modtime", + "empty-mode": "empty_mode", +} + + +def build_archive(args): + """Build the complete archive bytes.""" + output = bytearray() + output += ZOS_MAGIC + + if args.empty: + return bytes(output) + + # Parse members. + members = [] + if args.member: + for m in args.member: + parts = m.split(":", 1) + name = parts[0] + if len(parts) > 1: + data = parse_member_data(parts[1]) + else: + data = b"\x00" * 16 # Dummy content. + members.append((name, data)) + + # Parse symbols. + symbols = [] + if args.symtab: + for s in args.symtab: + parts = s.split(":") + sym_name = parts[0] + mem_idx = int(parts[1]) if len(parts) > 1 else 0 + attrs = int(parts[2]) if len(parts) > 2 else 0 + symbols.append((sym_name, mem_idx, attrs)) + + # Parse symtab header malformation flags. + symtab_hdr_kwargs = {} + if args.malform_symtab_hdr: + key = args.malform_symtab_hdr + if key not in _SYMTAB_HDR_MALFORMATIONS: + sys.exit( + f"Unknown --malform-symtab-hdr value: {key}. " + f"Valid: {', '.join(_SYMTAB_HDR_MALFORMATIONS.keys())}" + ) + symtab_hdr_kwargs[_SYMTAB_HDR_MALFORMATIONS[key]] = True + + # Phase 1: Compute member offsets. + # Start after magic. + pos = len(ZOS_MAGIC) + + # If we have a symbol table, it comes first. + symtab_body = None + has_symtab = ( + symbols + or args.symtab_no_symbols + or args.symtab_truncated + or args.symtab_bad_count + ) + if has_symtab: + # We need to compute the symtab size, but symtab contains member + # offsets, which depend on symtab size so we do two passes. + + # First pass: compute symtab body with placeholder offsets. + if args.symtab_truncated: + symtab_body = make_symtab([], [], truncated=True) + elif args.symtab_bad_count: + symtab_body = make_symtab([], [], bad_count=True) + elif args.symtab_no_symbols: + symtab_body = struct.pack(">I", 0) # 0 symbols. + else: + placeholder_offsets = [0] * (len(members) + 1) + symtab_body = make_symtab(symbols, placeholder_offsets) + + symtab_hdr_size = 60 # Fixed header for __.SYMDEF. + symtab_total = symtab_hdr_size + len(symtab_body) + # Padding to even boundary. + if symtab_total % 2 != 0: + symtab_total += 1 + pos += symtab_total + + # Compute member offsets. + member_offsets = [] + for name, data in members: + member_offsets.append(pos) + hdr_size = 60 + name_ext = 0 + if len(name) > 16: + name_ext = len(ascii_to_ebcdic(name)) + total = hdr_size + name_ext + len(data) + if total % 2 != 0: + total += 1 + pos += total + + # Second pass: recompute symtab with correct offsets. + if symbols and not args.symtab_truncated and not args.symtab_bad_count: + symtab_body = make_symtab(symbols, member_offsets) + + # Phase 2: Write output. + if symtab_body is not None: + symtab_hdr = make_member_header( + "__.SYMDEF", 0, 0, 0, 0, len(symtab_body), **symtab_hdr_kwargs + ) + output += symtab_hdr + output += symtab_body + # Pad to even boundary. + if len(output) % 2 != 0: + output += EBCDIC_NEWLINE + + for i, (name, data) in enumerate(members): + hdr = make_member_header( + name, + 1234567890, + 0, + 0, + 100644, + len(data), + bad_terminator=args.bad_terminator, + empty_name=args.empty_name, + empty_uid=args.empty_uid, + empty_gid=args.empty_gid, + empty_modtime=args.empty_modtime, + empty_mode=args.empty_mode, + ) + output += hdr + output += data + if len(output) % 2 != 0: + output += EBCDIC_NEWLINE + + return bytes(output) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate z/OS archive files for testing" + ) + parser.add_argument("--output", "-o", required=True, help="Output file path") + parser.add_argument( + "--empty", action="store_true", help="Create an empty archive (magic only)" + ) + parser.add_argument( + "--member", + action="append", + help="Add member as name[:data]. " + "Data can be a file path, hex:DEADBEEF, " + "or a raw ASCII string. If omitted, uses " + "16 zero bytes as dummy content.", + ) + parser.add_argument( + "--symtab", action="append", help="Add symbol: name[:member_index[:attributes]]" + ) + parser.add_argument( + "--symtab-no-symbols", + action="store_true", + help="Add empty symbol table (0 symbols)", + ) + parser.add_argument( + "--symtab-truncated", action="store_true", help="Create truncated symbol table" + ) + parser.add_argument( + "--symtab-bad-count", action="store_true", help="Symbol count exceeds buffer" + ) + parser.add_argument( + "--malform-symtab-hdr", + metavar="MALFORMATION", + help="Apply a malformation to the __.SYMDEF header. " + "Valid values: bad-terminator, empty-name, " + "empty-uid, empty-gid, empty-modtime, empty-mode", + ) + parser.add_argument( + "--bad-terminator", + action="store_true", + help="Use invalid terminator on member headers", + ) + parser.add_argument( + "--empty-name", + action="store_true", + help="Empty/space-leading name on member headers", + ) + parser.add_argument( + "--empty-uid", action="store_true", help="Empty UID on member headers" + ) + parser.add_argument( + "--empty-gid", action="store_true", help="Empty GID on member headers" + ) + parser.add_argument( + "--empty-modtime", + action="store_true", + help="Empty LastModified on member headers", + ) + parser.add_argument( + "--empty-mode", action="store_true", help="Empty AccessMode on member headers" + ) + args = parser.parse_args() + + data = build_archive(args) + with open(args.output, "wb") as f: + f.write(data) + + +if __name__ == "__main__": + main() diff --git a/llvm/test/Object/zos-archive-read.test b/llvm/test/Object/zos-archive-read.test new file mode 100644 index 0000000000000..8f01d13a09f36 --- /dev/null +++ b/llvm/test/Object/zos-archive-read.test @@ -0,0 +1,69 @@ +## Test reading a valid z/OS archive. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.valid.a \ +# RUN: --symtab "foo.txt:0" --symtab "bar:0" --member foo.txt:abcd +# RUN: llvm-ar t %t.valid.a | FileCheck %s --check-prefix=LIST +# RUN: llvm-nm --print-armap %t.valid.a | FileCheck %s --check-prefix=SYMS +# RUN: llvm-ar p %t.valid.a foo.txt | FileCheck %s --check-prefix=CONTENT + +# LIST: foo.txt +# SYMS: Archive map +# SYMS-NEXT: foo.txt in foo.txt +# SYMS-NEXT: bar in foo.txt +# CONTENT: abcd + +## Test malformed terminator on member header. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.bad_term.a \ +# RUN: --member foo.txt --bad-terminator +# RUN: not llvm-ar t %t.bad_term.a 2>&1 | FileCheck %s --check-prefix=ERR-TERM +# ERR-TERM: terminator characters in archive member + +## Test empty UID field on member header. +## We know that the member header starts at offset 8 because no +## symbol table name is generated. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.empty_uid.a \ +# RUN: --member foo.txt --empty-uid +# RUN: not llvm-ar t %t.empty_uid.a 2>&1 | FileCheck %s --check-prefix=ERR-UID +# ERR-UID: UID field is empty or contains only spaces in archive member header at offset 8 + +## Test leading space in member name. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.empty_name.a \ +# RUN: --member foo.txt --empty-name +# RUN: not llvm-ar t %t.empty_name.a 2>&1 | FileCheck %s --check-prefix=ERR-NAME +# ERR-NAME: name contains a leading space for archive member header + +## Test truncated z/OS symbol table. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.trunc_sym.a \ +# RUN: --symtab-truncated +# RUN: not llvm-ar t %t.trunc_sym.a 2>&1 | FileCheck %s --check-prefix=ERR-TRUNC +# ERR-TRUNC: z/OS archive symbol table is too small to read the symbol count, symbol table size is 2 +## Test z/OS symbol table where count exceeds buffer. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.bad_count.a \ +# RUN: --symtab-bad-count +# RUN: not llvm-ar t %t.bad_count.a 2>&1 | FileCheck %s --check-prefix=ERR-COUNT +# ERR-COUNT: z/OS archive symbol table names offset {{[0-9]+}} exceeds symbol table size 4 + +## Test malformed __.SYMDEF header. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.bad_sym_hdr.a \ +# RUN: --member foo.txt --symtab foo.txt:0 --malform-symtab-hdr empty-mode +# RUN: not llvm-ar t %t.bad_sym_hdr.a 2>&1 | FileCheck %s --check-prefix=ERR-SYM-HDR +# ERR-SYM-HDR: AccessMode field is empty or contains only spaces + +## Test empty LastModified field. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.empty_modtime.a \ +# RUN: --member foo.txt --empty-modtime +# RUN: not llvm-ar t %t.empty_modtime.a 2>&1 | FileCheck %s --check-prefix=ERR-MODTIME +# ERR-MODTIME: LastModified field is empty or contains only spaces in archive member header at offset 8 + +## Test empty GID field. +# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.empty_gid.a \ +# RUN: --member foo.txt --empty-gid +# RUN: not llvm-ar t %t.empty_gid.a 2>&1 | FileCheck %s --check-prefix=ERR-GID +# ERR-GID: GID field is empty or contains only spaces in archive member header at offset 8 + +## Test that a truncated archive with incomplete member header is rejected. +## The z/OS magic is 8 bytes and a member header requires 60 bytes. +## Writing only 3 bytes after the magic is not enough for a valid header. +# RUN: printf '\x5A\x4C\x81\x99\x83\x88\x6E\x15' > %t.badhdr.a +# RUN: printf '\x00\x00\x00' >> %t.badhdr.a +# RUN: not llvm-ar t %t.badhdr.a 2>&1 | FileCheck %s --check-prefix=ERR-CHILD +# ERR-CHILD: truncated or malformed archive