[llvm] [z/OS] Add z/OS archive reading support (PR #187110)

Uyiosa Iyekekpolor via llvm-commits llvm-commits at lists.llvm.org
Wed May 6 19:22:37 PDT 2026


https://github.com/uyoyo0 updated https://github.com/llvm/llvm-project/pull/187110

>From 12727a9a353bf7dead4aba1086b58e05a9a92cdf Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Tue, 17 Mar 2026 15:29:38 -0400
Subject: [PATCH 01/15] [z/OS] Add z/OS archive reading support

---
 llvm/include/llvm/Object/Archive.h         |  53 +++++-
 llvm/lib/Object/Archive.cpp                | 211 ++++++++++++++++++++-
 llvm/lib/Object/ArchiveWriter.cpp          |   3 +
 llvm/test/Object/Inputs/zos-archive-test.a | Bin 0 -> 1424 bytes
 llvm/test/Object/zos-archive-read.test     |   9 +
 5 files changed, 273 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/Object/Inputs/zos-archive-test.a
 create mode 100644 llvm/test/Object/zos-archive-read.test

diff --git a/llvm/include/llvm/Object/Archive.h b/llvm/include/llvm/Object/Archive.h
index c97018d3231d5..9a72460846502 100644
--- a/llvm/include/llvm/Object/Archive.h
+++ b/llvm/include/llvm/Object/Archive.h
@@ -158,6 +158,35 @@ class LLVM_ABI BigArchiveMemberHeader
   Expected<bool> isThin() const override { return false; }
 };
 
+// Define file member header of z/OS archive.
+class ZOSArchiveMemberHeader : public ArchiveMemberHeader {
+public:
+  ZOSArchiveMemberHeader(Archive const *Parent, const char *RawHeaderPtr,
+                         uint64_t Size, Error *Err);
+  std::unique_ptr<AbstractArchiveMemberHeader> clone() const override {
+    return std::make_unique<ZOSArchiveMemberHeader>(*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<StringRef> getRawName() const override;
+  Expected<StringRef> getName(uint64_t Size) const override;
+  StringRef getRawAccessMode() const override;
+  StringRef getRawLastModified() const override;
+  StringRef getRawUID() const override;
+  StringRef getRawGID() const override;
+  Expected<uint64_t> getSize() const override;
+  Expected<bool> isThin() const override { return false; }
+};
+
 class LLVM_ABI Archive : public Binary {
   virtual void anchor();
 
@@ -343,7 +372,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 +472,19 @@ 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.
+  };
+
+public:
+  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..c7a3e75f0f05e 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,11 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent,
     *Err = createMemberHeaderParseError(this, RawHeaderPtr, Size);
     return;
   }
-  if (ArMemHdr->Terminator[0] != '`' || ArMemHdr->Terminator[1] != '\n') {
+  if ((ArMemHdr->Terminator[0] != '`' || ArMemHdr->Terminator[1] != '\n') &&
+      (ArMemHdr->Terminator[0] != '\x79' ||
+       ArMemHdr->Terminator[1] !=
+           '\x15') // '\x79\x15' is '`\n' in EBCDIC for z/OS archive terminator.
+  ) {
     if (Err) {
       std::string Buf;
       raw_string_ostream OS(Buf);
@@ -368,6 +373,121 @@ Expected<uint64_t> BigArchiveMemberHeader::getSize() const {
   return *SizeOrErr + alignTo(*NameLenOrErr, 2);
 }
 
+template <class T, std::size_t N>
+StringRef getFieldRawStringE2A(const T (&Field)[N], SmallString<64> &Dst) {
+  StringRef Src = StringRef(Field, N);
+  ConverterEBCDIC::convertToUTF8(Src, Dst);
+  return Dst.str().rtrim(" ");
+}
+
+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<uint64_t> ZOSArchiveMemberHeader::getSize() const {
+  SmallString<64> Dst;
+  return getArchiveMemberDecField(
+      "size", getFieldRawStringE2A(ArMemHdr->Size, Dst), Parent, this);
+}
+
+Expected<StringRef> ZOSArchiveMemberHeader::getRawName() const {
+  return StringRef(RawMemberName);
+}
+
+Expected<StringRef> 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) {
+  SmallString<64> Dst;
+  uint64_t Offset =
+      reinterpret_cast<const char *>(ArMemHdr) - Parent->getData().data();
+
+  // Set RawMemberName.
+  StringRef RawNameSR = getFieldRawStringE2A(ArMemHdr->Name, Dst);
+  if (RawNameSR.empty() || RawNameSR[0] == ' ') {
+    *Err = malformedError("name contains a leading space for archive member "
+                          "header at offset " +
+                          Twine(Offset));
+    return;
+  }
+  RawMemberName.append(RawNameSR);
+
+  // Set MemberName.
+  if (RawNameSR.starts_with("#1/")) {
+    Expected<StringRef> NameOrErr = ArchiveMemberHeader::getName(Size);
+    if (!NameOrErr) {
+      *Err = NameOrErr.takeError();
+      return;
+    }
+    StringRef Name = NameOrErr.get();
+    Dst.clear();
+    ConverterEBCDIC::convertToUTF8(Name, Dst);
+    MemberName.append(Dst.str());
+  } else
+    MemberName = RawMemberName;
+
+  // LastModified
+  Dst.clear();
+  StringRef LastModifiedSR = getFieldRawStringE2A(ArMemHdr->LastModified, Dst);
+  if (LastModifiedSR.empty()) {
+    *Err = malformedError("problem converting LastModified field in "
+                          "header at offset " +
+                          Twine(Offset));
+    return;
+  }
+  LastModified.append(LastModifiedSR);
+
+  // UID
+  Dst.clear();
+  StringRef UIDSR = getFieldRawStringE2A(ArMemHdr->UID, Dst);
+  if (UIDSR.empty()) {
+    *Err = malformedError("problem converting UID field in "
+                          "header at offset " +
+                          Twine(Offset));
+    return;
+  }
+  UID.append(UIDSR);
+
+  // GID
+  Dst.clear();
+  StringRef GIDSR = getFieldRawStringE2A(ArMemHdr->GID, Dst);
+  if (GIDSR.empty()) {
+    *Err = malformedError("problem converting GID field in "
+                          "header at offset " +
+                          Twine(Offset));
+    return;
+  }
+  GID.append(GIDSR);
+
+  // AccessMode
+  Dst.clear();
+  StringRef AccessModeSR = getFieldRawStringE2A(ArMemHdr->AccessMode, Dst);
+  if (AccessModeSR.empty()) {
+    *Err = malformedError("problem converting AccessMode field in "
+                          "header at offset " +
+                          Twine(Offset));
+    return;
+  }
+  AccessMode.append(AccessModeSR);
+}
+
 Expected<uint64_t> BigArchiveMemberHeader::getRawNameSize() const {
   return getArchiveMemberDecField(
       "NameLen", getFieldRawString(ArMemHdr->NameLen), Parent, this);
@@ -668,6 +788,8 @@ Expected<std::unique_ptr<Archive>> Archive::create(MemoryBufferRef Source) {
 
   if (Buffer.starts_with(BigArchiveMagic))
     Ret = std::make_unique<BigArchive>(Source, Err);
+  else if (Buffer.starts_with(ZOSArchiveMagic))
+    Ret = std::make_unique<ZOSArchive>(Source, Err);
   else
     Ret = std::make_unique<Archive>(Source, Err);
 
@@ -680,6 +802,10 @@ std::unique_ptr<AbstractArchiveMemberHeader>
 Archive::createArchiveMemberHeader(const char *RawHeaderPtr, uint64_t Size,
                                    Error *Err) const {
   ErrorAsOutParameter ErrAsOutParam(Err);
+
+  if (kind() == K_ZOS)
+    return std::make_unique<ZOSArchiveMemberHeader>(this, RawHeaderPtr, Size,
+                                                    Err);
   if (kind() != K_AIXBIG)
     return std::make_unique<ArchiveMemberHeader>(this, RawHeaderPtr, Size, Err);
   return std::make_unique<BigArchiveMemberHeader>(this, RawHeaderPtr, Size,
@@ -695,7 +821,6 @@ uint64_t Archive::getArchiveMagicLen() const {
 
   return sizeof(ArchiveMagic) - 1;
 }
-
 void Archive::setFirstRegular(const Child &C) {
   FirstRegularData = C.Data;
   FirstRegularStartOfFile = C.StartOfFile;
@@ -714,6 +839,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<GenericBinaryError>("file too small to be an archive",
                                          object_error::invalid_file_type);
@@ -971,6 +1100,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 +1173,8 @@ Expected<Archive::Child> 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) {
+    Offset = read32be(Offsets + SymbolIndex * 8);
   } else {
     // Skip offsets.
     uint32_t MemberCount = read32le(Buf);
@@ -1171,6 +1304,9 @@ Archive::symbol_iterator Archive::symbol_begin() const {
     buf += ran_strx;
   } else if (kind() == K_AIXBIG) {
     buf = getStringTable().begin();
+  } else if (kind() == K_ZOS) {
+    uint32_t symbol_count = read32be(buf);
+    buf += sizeof(uint32_t) + (symbol_count * (sizeof(uint64_t)));
   } else {
     uint32_t member_count = 0;
     uint32_t symbol_count = 0;
@@ -1244,6 +1380,9 @@ 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 +1587,71 @@ 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;
+
+  auto Increment = [&]() {
+    ++I;
+    if (Err)
+      return true;
+    C = &*I;
+    return false;
+  };
+
+  Expected<StringRef> NameOrErr = C->getRawName();
+  if (!NameOrErr) {
+    Err = NameOrErr.takeError();
+    return;
+  }
+  StringRef Name = NameOrErr.get();
+
+  if (Name == "__.SYMDEF") {
+    // We know that the symbol table is not an external file, but we still must
+    // check any Expected<> return value.
+    Expected<StringRef> BufOrErr = C->getBuffer();
+    if (!BufOrErr) {
+      Err = BufOrErr.takeError();
+      return;
+    }
+
+    // Copy symbol table converting embedded EBCDIC names to ASCII.
+    StringRef ESymbolTable = BufOrErr.get();
+    uint32_t ESymbolCount = read32be(ESymbolTable.data());
+    uint32_t OffsetToENames =
+        sizeof(uint32_t) + (ESymbolCount * (sizeof(uint64_t)));
+    uint32_t ENamesSize = (uint32_t)ESymbolTable.size() - OffsetToENames;
+    const char *ENamesPtr = (const char *)ESymbolTable.data() + OffsetToENames;
+    StringRef ENames(ENamesPtr, ENamesSize);
+
+    SmallString<64> Dst;
+    ConverterEBCDIC::convertToUTF8(ENames, Dst);
+    SymbolTableBuf.append(ESymbolTable.data(), OffsetToENames);
+    SymbolTableBuf.append(Dst.str());
+    SymbolTable = StringRef(SymbolTableBuf.data(), SymbolTableBuf.size());
+    if (Increment())
+      return;
+    setFirstRegular(*C);
+
+    Err = Error::success();
+    return;
+  }
+
+  setFirstRegular(*C);
+  Err = Error::success();
+  return;
+}
\ No newline at end of file
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/zos-archive-test.a b/llvm/test/Object/Inputs/zos-archive-test.a
new file mode 100644
index 0000000000000000000000000000000000000000..0244f176c64478cec3918b1ca6bed78b897eb2c1
GIT binary patch
literal 1424
zcmbtUPfO!K6n}C0?;uOvZJ{h8rFa%ULOkuEMS4&O;=wPFy^9_q3yVkbQ1K`T{Twyz
zQZEwNMe!n`dT>uWjcN2`_q~}U9b at CN_~6a^|C{$_CfR809e>;ZS~ku4;Qm*)_d}P&
zLrFsF>Z<F*AWGOS1AvCSM*O|i0_1ut3di-*p`rx7FPuZ~Dco+m%s8U_AJN{a#AM{n
zoYZ1~EioBD7H8VAF%j_yYX&)q*;jEiOSM9sK>;8`M$R_v%WhBL5f-&cGL at 2>Q6d)2
zr1VFdOe=};nH8%Og=yFywE_j(+}*vp>Ue08Ba18=r(j5!J0Vt`=!&L+Ch&6g=F!of
zC%~w$9CEV1no<YT&#zbjOX8iG=iTd{0+0I2Pr*~+UTr3V49`zaRj_-#!ENXI7SZV*
zXtwA+mR|scMzVQwi(1hhF!~OJZ%&8^OBlHH8PsH^^i-mN$Gw0*3?GMqKk|J3z>#pg
z{1?tk8aBy3QKp5wi7xHn&Y3k_r?CRAwGMWvy$GcyxQ)Z23r)w(RBjC3@!L<J8IF^|
z%vnQs7PAWmx92OX*+0NO5?<RM-+#V1$&JTjhisx}Dd2#l4n<s-Fn$`krHJQ at yo~bX
zdx=~mU%VAH at Gl+lqW7Yv*6^?wM!{(G`*J{k7(A!M$#FG(qO&UM%XGQ&;>GKx+sSOv
Xf{c`};-bXq^`{Wi<G_bldx!EDV(Pt2

literal 0
HcmV?d00001

diff --git a/llvm/test/Object/zos-archive-read.test b/llvm/test/Object/zos-archive-read.test
new file mode 100644
index 0000000000000..7adf2a5256697
--- /dev/null
+++ b/llvm/test/Object/zos-archive-read.test
@@ -0,0 +1,9 @@
+## Test reading a z/OS archive.
+
+# RUN: llvm-ar t %p/Inputs/zos-archive-test.a | FileCheck %s --check-prefix=LIST
+# RUN: llvm-nm --print-armap %p/Inputs/zos-archive-test.a | FileCheck %s --check-prefix=SYMS
+
+# LIST: foo.o
+
+# SYMS: Archive map
+# SYMS-NEXT: foo in foo.o
\ No newline at end of file

>From 12e3e4b6314642020707919ab0218bd31803d6ba Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Thu, 9 Apr 2026 10:51:04 -0400
Subject: [PATCH 02/15] Address review comments

---
 llvm/include/llvm/Object/Archive.h | 1 -
 llvm/lib/Object/Archive.cpp        | 6 +++---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/llvm/include/llvm/Object/Archive.h b/llvm/include/llvm/Object/Archive.h
index 9a72460846502..b45a8e4673402 100644
--- a/llvm/include/llvm/Object/Archive.h
+++ b/llvm/include/llvm/Object/Archive.h
@@ -479,7 +479,6 @@ class ZOSArchive : public Archive {
     char Magic[sizeof(ZOSArchiveMagic) - 1]; ///< ZOS archive magic string.
   };
 
-public:
   ZOSArchive(MemoryBufferRef Source, Error &Err);
 
 private:
diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index c7a3e75f0f05e..1554161eec34b 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -373,8 +373,8 @@ Expected<uint64_t> BigArchiveMemberHeader::getSize() const {
   return *SizeOrErr + alignTo(*NameLenOrErr, 2);
 }
 
-template <class T, std::size_t N>
-StringRef getFieldRawStringE2A(const T (&Field)[N], SmallString<64> &Dst) {
+template <std::size_t N>
+StringRef getFieldRawStringE2A(const char (&Field)[N], SmallString<64> &Dst) {
   StringRef Src = StringRef(Field, N);
   ConverterEBCDIC::convertToUTF8(Src, Dst);
   return Dst.str().rtrim(" ");
@@ -1654,4 +1654,4 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
   setFirstRegular(*C);
   Err = Error::success();
   return;
-}
\ No newline at end of file
+}

>From 1d9da96ac58bab96f1f247d86b85c202e8b29502 Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Thu, 9 Apr 2026 13:36:25 -0400
Subject: [PATCH 03/15] Address additional review comments

---
 llvm/include/llvm/Object/Archive.h | 10 ++++++++++
 llvm/lib/Object/Archive.cpp        | 17 ++++++++++-------
 2 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/llvm/include/llvm/Object/Archive.h b/llvm/include/llvm/Object/Archive.h
index b45a8e4673402..59eca4babd60d 100644
--- a/llvm/include/llvm/Object/Archive.h
+++ b/llvm/include/llvm/Object/Archive.h
@@ -159,6 +159,16 @@ class LLVM_ABI BigArchiveMemberHeader
 };
 
 // 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]; /* blank terminated 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,
diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 1554161eec34b..98690a554356f 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -375,6 +375,7 @@ Expected<uint64_t> BigArchiveMemberHeader::getSize() const {
 
 template <std::size_t N>
 StringRef getFieldRawStringE2A(const char (&Field)[N], SmallString<64> &Dst) {
+  Dst.clear();
   StringRef Src = StringRef(Field, N);
   ConverterEBCDIC::convertToUTF8(Src, Dst);
   return Dst.str().rtrim(" ");
@@ -444,7 +445,6 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
     MemberName = RawMemberName;
 
   // LastModified
-  Dst.clear();
   StringRef LastModifiedSR = getFieldRawStringE2A(ArMemHdr->LastModified, Dst);
   if (LastModifiedSR.empty()) {
     *Err = malformedError("problem converting LastModified field in "
@@ -455,7 +455,6 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   LastModified.append(LastModifiedSR);
 
   // UID
-  Dst.clear();
   StringRef UIDSR = getFieldRawStringE2A(ArMemHdr->UID, Dst);
   if (UIDSR.empty()) {
     *Err = malformedError("problem converting UID field in "
@@ -466,7 +465,6 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   UID.append(UIDSR);
 
   // GID
-  Dst.clear();
   StringRef GIDSR = getFieldRawStringE2A(ArMemHdr->GID, Dst);
   if (GIDSR.empty()) {
     *Err = malformedError("problem converting GID field in "
@@ -477,7 +475,6 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   GID.append(GIDSR);
 
   // AccessMode
-  Dst.clear();
   StringRef AccessModeSR = getFieldRawStringE2A(ArMemHdr->AccessMode, Dst);
   if (AccessModeSR.empty()) {
     *Err = malformedError("problem converting AccessMode field in "
@@ -1174,6 +1171,12 @@ Expected<Archive::Child> Archive::Symbol::getMember() const {
     // is needed here.
     Offset = read64le(Offsets + SymbolIndex * 16 + 8);
   } else if (Parent->kind() == K_ZOS) {
+    // The contents of the symbol table member in order are:
+    // 1. The number of symbols, NS (4 byte integer).
+    // 2. NS pairs of integers (the first in each pair of integers is the offset 
+    //    to the header of the entry... the second being coded attributes). 
+    //    Length is NS*(4+4) bytes.
+    // 3. NS null terminated strings of corresponding symbol names.
     Offset = read32be(Offsets + SymbolIndex * 8);
   } else {
     // Skip offsets.
@@ -1608,9 +1611,9 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
   auto Increment = [&]() {
     ++I;
     if (Err)
-      return true;
+      return false;
     C = &*I;
-    return false;
+    return true;
   };
 
   Expected<StringRef> NameOrErr = C->getRawName();
@@ -1643,7 +1646,7 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     SymbolTableBuf.append(ESymbolTable.data(), OffsetToENames);
     SymbolTableBuf.append(Dst.str());
     SymbolTable = StringRef(SymbolTableBuf.data(), SymbolTableBuf.size());
-    if (Increment())
+    if (!Increment())
       return;
     setFirstRegular(*C);
 

>From 1b28b03be296581ef447824bf68c68acb920e015 Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Thu, 9 Apr 2026 13:51:06 -0400
Subject: [PATCH 04/15] Run clang-format

---
 llvm/include/llvm/Object/Archive.h | 3 ++-
 llvm/lib/Object/Archive.cpp        | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/llvm/include/llvm/Object/Archive.h b/llvm/include/llvm/Object/Archive.h
index 59eca4babd60d..cff8367e891ab 100644
--- a/llvm/include/llvm/Object/Archive.h
+++ b/llvm/include/llvm/Object/Archive.h
@@ -167,7 +167,8 @@ class LLVM_ABI BigArchiveMemberHeader
 //   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') */
+//   char ar_fmag[2];  /* contains backtick (X'79'), followed by new line
+//   (X'15') */
 // };
 class ZOSArchiveMemberHeader : public ArchiveMemberHeader {
 public:
diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 98690a554356f..375a57126bfad 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -1173,8 +1173,8 @@ Expected<Archive::Child> Archive::Symbol::getMember() const {
   } else if (Parent->kind() == K_ZOS) {
     // The contents of the symbol table member in order are:
     // 1. The number of symbols, NS (4 byte integer).
-    // 2. NS pairs of integers (the first in each pair of integers is the offset 
-    //    to the header of the entry... the second being coded attributes). 
+    // 2. NS pairs of integers (the first in each pair of integers is the offset
+    //    to the header of the entry... the second being coded attributes).
     //    Length is NS*(4+4) bytes.
     // 3. NS null terminated strings of corresponding symbol names.
     Offset = read32be(Offsets + SymbolIndex * 8);

>From 8cef225f1d00739788ada50e8b506ea7f993057c Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Thu, 9 Apr 2026 15:05:54 -0400
Subject: [PATCH 05/15] Move Increment lambda closer to its first use

---
 llvm/lib/Object/Archive.cpp | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 375a57126bfad..90479b0330cbf 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -1608,14 +1608,6 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
   }
   const Child *C = &*I;
 
-  auto Increment = [&]() {
-    ++I;
-    if (Err)
-      return false;
-    C = &*I;
-    return true;
-  };
-
   Expected<StringRef> NameOrErr = C->getRawName();
   if (!NameOrErr) {
     Err = NameOrErr.takeError();
@@ -1646,6 +1638,15 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     SymbolTableBuf.append(ESymbolTable.data(), OffsetToENames);
     SymbolTableBuf.append(Dst.str());
     SymbolTable = StringRef(SymbolTableBuf.data(), SymbolTableBuf.size());
+
+    auto Increment = [&]() {
+      ++I;
+      if (Err)
+        return false;
+      C = &*I;
+      return true;
+    };
+
     if (!Increment())
       return;
     setFirstRegular(*C);

>From 6fd2aae260b150d60f1d4f1d1c0e82fe1cf75fa1 Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Mon, 13 Apr 2026 09:53:02 -0400
Subject: [PATCH 06/15] Clarify z/OS symbol table offset math and layout
 comments

---
 llvm/lib/Object/Archive.cpp | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 90479b0330cbf..694a054aedae5 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -1171,12 +1171,10 @@ Expected<Archive::Child> Archive::Symbol::getMember() const {
     // is needed here.
     Offset = read64le(Offsets + SymbolIndex * 16 + 8);
   } else if (Parent->kind() == K_ZOS) {
-    // The contents of the symbol table member in order are:
-    // 1. The number of symbols, NS (4 byte integer).
-    // 2. NS pairs of integers (the first in each pair of integers is the offset
-    //    to the header of the entry... the second being coded attributes).
-    //    Length is NS*(4+4) bytes.
-    // 3. NS null terminated strings of corresponding symbol names.
+    // 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.
@@ -1308,6 +1306,12 @@ Archive::symbol_iterator Archive::symbol_begin() const {
   } 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 symbol_count = read32be(buf);
     buf += sizeof(uint32_t) + (symbol_count * (sizeof(uint64_t)));
   } else {

>From bda2dff8bd163e4f31d5a6f03dee6dab1d3e5b4f Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Mon, 13 Apr 2026 11:36:44 -0400
Subject: [PATCH 07/15] Add test for extracting z/OS archive member

---
 llvm/test/Object/Inputs/zos-expected-foo.o | Bin 0 -> 1280 bytes
 llvm/test/Object/zos-archive-read.test     |   6 +++++-
 2 files changed, 5 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/Object/Inputs/zos-expected-foo.o

diff --git a/llvm/test/Object/Inputs/zos-expected-foo.o b/llvm/test/Object/Inputs/zos-expected-foo.o
new file mode 100644
index 0000000000000000000000000000000000000000..4caf23315e4c9f140c3a0c11b92af7e0dfc77c7c
GIT binary patch
literal 1280
zcmbtSy-EW?5T4EE=L(UiDT0WEU at bnvSy?EOM!3QvFOVnD;%uzNLTf?DbHp>CjT{I{
z8abkcR<4)3Xl0z)y`S|=BI3aA{D1rH?r;Hr6*jGbtK`(0Dg8}QorS}blRXCu5sxA{
zTv^O39f#Sv7ug&PfIK-xr{&yTb|oHR5lzw)C3_eZVpDDcKRRSwOM=hs#7qs&kRMS2
z!)fjB-`{t9^e9lkC5clq60A*y*HT-v7%+)f+-#hl9{3WBcolJF`PH>P82^4{18jwN
z(Yn6u-AX*-RT_h*!~Hl+CON*Jx|QK{yZy&b?-9}IALwrJf2 at 52ikvKY3d>$e4>-<?
ztmJc2#aPn8?TSNPRjNoePWZwfiox6a+b{_IKnw&5UazzvM1>(qzCe{Wu3ZeVqbGO9
z^4zU;@a!FMD(w}hG{D<BE_={$y?ph-5}ly^0nBpUJXX$Frn^#DwuG}(-7dTWpC`O_
zFnV6RIV+AvBbR)VPo?09v<YS0S22Gcg_TO~8AmzQsd<S*q`%FIp7_m1ym(&p(mFo&
zaySgbm%F}cn)FVYmlJw<YV#V}YxKG5<K=YI at 8p}b;X*0ZaMSWM{S$<nNn$p!{$YFp
DKWm9X

literal 0
HcmV?d00001

diff --git a/llvm/test/Object/zos-archive-read.test b/llvm/test/Object/zos-archive-read.test
index 7adf2a5256697..2afcb1b79ee22 100644
--- a/llvm/test/Object/zos-archive-read.test
+++ b/llvm/test/Object/zos-archive-read.test
@@ -3,7 +3,11 @@
 # RUN: llvm-ar t %p/Inputs/zos-archive-test.a | FileCheck %s --check-prefix=LIST
 # RUN: llvm-nm --print-armap %p/Inputs/zos-archive-test.a | FileCheck %s --check-prefix=SYMS
 
+## Test extracting a member and verifying its content.
+# RUN: llvm-ar p %p/Inputs/zos-archive-test.a foo.o > %t.foo.o
+# RUN: cmp %t.foo.o %p/Inputs/zos-expected-foo.o
+
 # LIST: foo.o
 
 # SYMS: Archive map
-# SYMS-NEXT: foo in foo.o
\ No newline at end of file
+# SYMS-NEXT: foo in foo.o

>From bd6299787174aa6677e04263578bf4e37da0201b Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Wed, 15 Apr 2026 16:46:27 -0400
Subject: [PATCH 08/15] Address reviewer comments

---
 llvm/include/llvm/Object/Archive.h |   2 +-
 llvm/lib/Object/Archive.cpp        | 125 +++++++++++++++--------------
 2 files changed, 64 insertions(+), 63 deletions(-)

diff --git a/llvm/include/llvm/Object/Archive.h b/llvm/include/llvm/Object/Archive.h
index cff8367e891ab..cdb7d32311e4a 100644
--- a/llvm/include/llvm/Object/Archive.h
+++ b/llvm/include/llvm/Object/Archive.h
@@ -161,7 +161,7 @@ class LLVM_ABI BigArchiveMemberHeader
 // 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]; /* blank terminated member name */
+//   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) */
diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 694a054aedae5..62cbca5cd37b3 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -105,11 +105,12 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent,
     *Err = createMemberHeaderParseError(this, RawHeaderPtr, Size);
     return;
   }
-  if ((ArMemHdr->Terminator[0] != '`' || ArMemHdr->Terminator[1] != '\n') &&
-      (ArMemHdr->Terminator[0] != '\x79' ||
-       ArMemHdr->Terminator[1] !=
-           '\x15') // '\x79\x15' is '`\n' in EBCDIC for 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);
@@ -123,8 +124,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;
   }
@@ -374,11 +376,11 @@ Expected<uint64_t> BigArchiveMemberHeader::getSize() const {
 }
 
 template <std::size_t N>
-StringRef getFieldRawStringE2A(const char (&Field)[N], SmallString<64> &Dst) {
+StringRef ebcdicFieldToASCII(const char (&Field)[N], SmallVectorImpl<char> &Dst) {
   Dst.clear();
   StringRef Src = StringRef(Field, N);
   ConverterEBCDIC::convertToUTF8(Src, Dst);
-  return Dst.str().rtrim(" ");
+  return StringRef(Dst.data(), Dst.size()).rtrim(" ");
 }
 
 ZOSArchiveMemberHeader::ZOSArchiveMemberHeader(const Archive *Parent,
@@ -392,7 +394,7 @@ ZOSArchiveMemberHeader::ZOSArchiveMemberHeader(const Archive *Parent,
 Expected<uint64_t> ZOSArchiveMemberHeader::getSize() const {
   SmallString<64> Dst;
   return getArchiveMemberDecField(
-      "size", getFieldRawStringE2A(ArMemHdr->Size, Dst), Parent, this);
+      "size", ebcdicFieldToASCII(ArMemHdr->Size, Dst), Parent, this);
 }
 
 Expected<StringRef> ZOSArchiveMemberHeader::getRawName() const {
@@ -420,18 +422,18 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   uint64_t Offset =
       reinterpret_cast<const char *>(ArMemHdr) - Parent->getData().data();
 
-  // Set RawMemberName.
-  StringRef RawNameSR = getFieldRawStringE2A(ArMemHdr->Name, Dst);
-  if (RawNameSR.empty() || RawNameSR[0] == ' ') {
+  // Set RawMemberName
+  StringRef RawNameRef = ebcdicFieldToASCII(ArMemHdr->Name, Dst);
+  if (RawNameRef.empty() || RawNameRef[0] == ' ') {
     *Err = malformedError("name contains a leading space for archive member "
                           "header at offset " +
                           Twine(Offset));
     return;
   }
-  RawMemberName.append(RawNameSR);
+  RawMemberName.append(RawNameRef);
 
   // Set MemberName.
-  if (RawNameSR.starts_with("#1/")) {
+  if (RawNameRef.starts_with("#1/")) {
     Expected<StringRef> NameOrErr = ArchiveMemberHeader::getName(Size);
     if (!NameOrErr) {
       *Err = NameOrErr.takeError();
@@ -441,48 +443,45 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
     Dst.clear();
     ConverterEBCDIC::convertToUTF8(Name, Dst);
     MemberName.append(Dst.str());
-  } else
+  } else {
     MemberName = RawMemberName;
+  }
 
   // LastModified
-  StringRef LastModifiedSR = getFieldRawStringE2A(ArMemHdr->LastModified, Dst);
-  if (LastModifiedSR.empty()) {
-    *Err = malformedError("problem converting LastModified field in "
-                          "header at offset " +
-                          Twine(Offset));
+  StringRef LastModifiedRef = ebcdicFieldToASCII(ArMemHdr->LastModified, Dst);
+  if (LastModifiedRef.empty()) {
+    *Err = malformedError("LastModified field is empty or contains only spaces in "
+                              "archive member header at offset " + Twine(Offset));
     return;
   }
-  LastModified.append(LastModifiedSR);
+  LastModified.append(LastModifiedRef);
 
   // UID
-  StringRef UIDSR = getFieldRawStringE2A(ArMemHdr->UID, Dst);
-  if (UIDSR.empty()) {
-    *Err = malformedError("problem converting UID field in "
-                          "header at offset " +
-                          Twine(Offset));
+  StringRef UIDRef = ebcdicFieldToASCII(ArMemHdr->UID, Dst);
+  if (UIDRef.empty()) {
+    *Err = malformedError("UID field is empty or contains only spaces in "
+                          "archive member header at offset " + Twine(Offset));
     return;
   }
-  UID.append(UIDSR);
+  UID.append(UIDRef);
 
   // GID
-  StringRef GIDSR = getFieldRawStringE2A(ArMemHdr->GID, Dst);
-  if (GIDSR.empty()) {
-    *Err = malformedError("problem converting GID field in "
-                          "header at offset " +
-                          Twine(Offset));
+  StringRef GIDRef = ebcdicFieldToASCII(ArMemHdr->GID, Dst);
+  if (GIDRef.empty()) {
+    *Err = malformedError("GID field is empty or contains only spaces in "
+                          "archive member header at offset " + Twine(Offset));
     return;
   }
-  GID.append(GIDSR);
+  GID.append(GIDRef);
 
   // AccessMode
-  StringRef AccessModeSR = getFieldRawStringE2A(ArMemHdr->AccessMode, Dst);
-  if (AccessModeSR.empty()) {
-    *Err = malformedError("problem converting AccessMode field in "
-                          "header at offset " +
-                          Twine(Offset));
+  StringRef AccessModeRef = ebcdicFieldToASCII(ArMemHdr->AccessMode, Dst);
+  if (AccessModeRef.empty()) {
+    *Err = malformedError("AccessMode field is empty or contains only spaces in "
+                          "archive member header at offset " + Twine(Offset));
     return;
   }
-  AccessMode.append(AccessModeSR);
+  AccessMode.append(AccessModeRef);
 }
 
 Expected<uint64_t> BigArchiveMemberHeader::getRawNameSize() const {
@@ -818,6 +817,7 @@ uint64_t Archive::getArchiveMagicLen() const {
 
   return sizeof(ArchiveMagic) - 1;
 }
+
 void Archive::setFirstRegular(const Child &C) {
   FirstRegularData = C.Data;
   FirstRegularStartOfFile = C.StartOfFile;
@@ -1312,8 +1312,8 @@ Archive::symbol_iterator Archive::symbol_begin() const {
     // 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 symbol_count = read32be(buf);
-    buf += sizeof(uint32_t) + (symbol_count * (sizeof(uint64_t)));
+    uint32_t SymbolCount = read32be(buf);
+    buf += sizeof(uint32_t) + (SymbolCount * (sizeof(uint64_t)));
   } else {
     uint32_t member_count = 0;
     uint32_t symbol_count = 0;
@@ -1387,9 +1387,8 @@ uint32_t Archive::getNumberOfSymbols() const {
     return read32le(buf) / 8;
   if (kind() == K_DARWIN64)
     return read64le(buf) / 16;
-  if (kind() == K_ZOS) {
+  if (kind() == K_ZOS) 
     return read32be(buf);
-  }
   uint32_t member_count = 0;
   member_count = read32le(buf);
   buf += 4 + (member_count * 4); // Skip offsets.
@@ -1629,32 +1628,34 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     }
 
     // Copy symbol table converting embedded EBCDIC names to ASCII.
-    StringRef ESymbolTable = BufOrErr.get();
-    uint32_t ESymbolCount = read32be(ESymbolTable.data());
-    uint32_t OffsetToENames =
-        sizeof(uint32_t) + (ESymbolCount * (sizeof(uint64_t)));
-    uint32_t ENamesSize = (uint32_t)ESymbolTable.size() - OffsetToENames;
-    const char *ENamesPtr = (const char *)ESymbolTable.data() + OffsetToENames;
-    StringRef ENames(ENamesPtr, ENamesSize);
+    StringRef EbcdicSymbolTable = BufOrErr.get();
+    if (EbcdicSymbolTable.size() < sizeof(uint32_t)) {
+      Err = malformedError("z/OS symbol table is too small to read the symbol count");
+      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 symbol table count exceeds buffer size");
+      return;
+    }
+    uint64_t EbcdicNamesSize = EbcdicSymbolTable.size() - OffsetToEbcdicNames;
+    const char *EbcdicNamesPtr = EbcdicSymbolTable.data() + OffsetToEbcdicNames;
+    StringRef EbcdicNames(EbcdicNamesPtr, EbcdicNamesSize);
 
     SmallString<64> Dst;
-    ConverterEBCDIC::convertToUTF8(ENames, Dst);
-    SymbolTableBuf.append(ESymbolTable.data(), OffsetToENames);
+    ConverterEBCDIC::convertToUTF8(EbcdicNames, Dst);
+    SymbolTableBuf.append(EbcdicSymbolTable.data(), OffsetToEbcdicNames);
     SymbolTableBuf.append(Dst.str());
     SymbolTable = StringRef(SymbolTableBuf.data(), SymbolTableBuf.size());
 
-    auto Increment = [&]() {
-      ++I;
-      if (Err)
-        return false;
-      C = &*I;
-      return true;
-    };
-
-    if (!Increment())
+    ++I;
+    if (Err)
       return;
+    C = &*I;
+    
     setFirstRegular(*C);
-
     Err = Error::success();
     return;
   }

>From 40363d970ffe9c26957faba02752f352125b56bc Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Wed, 15 Apr 2026 16:48:54 -0400
Subject: [PATCH 09/15] Address reviewer comments

---
 llvm/lib/Object/Archive.cpp | 36 ++++++++++++++++++++++--------------
 1 file changed, 22 insertions(+), 14 deletions(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 62cbca5cd37b3..b9f0674423000 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -106,10 +106,10 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent,
     return;
   }
   bool ValidTerminator =
-    Parent->kind() == Archive::K_ZOS ? (ArMemHdr->Terminator[0] == '\x79' &&
-              ArMemHdr->Terminator[1] == '\x15')
-          : (ArMemHdr->Terminator[0] == '`' &&
-              ArMemHdr->Terminator[1] == '\n');
+      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;
@@ -376,7 +376,8 @@ Expected<uint64_t> BigArchiveMemberHeader::getSize() const {
 }
 
 template <std::size_t N>
-StringRef ebcdicFieldToASCII(const char (&Field)[N], SmallVectorImpl<char> &Dst) {
+StringRef ebcdicFieldToASCII(const char (&Field)[N],
+                             SmallVectorImpl<char> &Dst) {
   Dst.clear();
   StringRef Src = StringRef(Field, N);
   ConverterEBCDIC::convertToUTF8(Src, Dst);
@@ -450,8 +451,10 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   // LastModified
   StringRef LastModifiedRef = ebcdicFieldToASCII(ArMemHdr->LastModified, Dst);
   if (LastModifiedRef.empty()) {
-    *Err = malformedError("LastModified field is empty or contains only spaces in "
-                              "archive member header at offset " + Twine(Offset));
+    *Err =
+        malformedError("LastModified field is empty or contains only spaces in "
+                       "archive member header at offset " +
+                       Twine(Offset));
     return;
   }
   LastModified.append(LastModifiedRef);
@@ -460,7 +463,8 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   StringRef UIDRef = ebcdicFieldToASCII(ArMemHdr->UID, Dst);
   if (UIDRef.empty()) {
     *Err = malformedError("UID field is empty or contains only spaces in "
-                          "archive member header at offset " + Twine(Offset));
+                          "archive member header at offset " +
+                          Twine(Offset));
     return;
   }
   UID.append(UIDRef);
@@ -469,7 +473,8 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   StringRef GIDRef = ebcdicFieldToASCII(ArMemHdr->GID, Dst);
   if (GIDRef.empty()) {
     *Err = malformedError("GID field is empty or contains only spaces in "
-                          "archive member header at offset " + Twine(Offset));
+                          "archive member header at offset " +
+                          Twine(Offset));
     return;
   }
   GID.append(GIDRef);
@@ -477,8 +482,10 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
   // AccessMode
   StringRef AccessModeRef = ebcdicFieldToASCII(ArMemHdr->AccessMode, Dst);
   if (AccessModeRef.empty()) {
-    *Err = malformedError("AccessMode field is empty or contains only spaces in "
-                          "archive member header at offset " + Twine(Offset));
+    *Err =
+        malformedError("AccessMode field is empty or contains only spaces in "
+                       "archive member header at offset " +
+                       Twine(Offset));
     return;
   }
   AccessMode.append(AccessModeRef);
@@ -1387,7 +1394,7 @@ uint32_t Archive::getNumberOfSymbols() const {
     return read32le(buf) / 8;
   if (kind() == K_DARWIN64)
     return read64le(buf) / 16;
-  if (kind() == K_ZOS) 
+  if (kind() == K_ZOS)
     return read32be(buf);
   uint32_t member_count = 0;
   member_count = read32le(buf);
@@ -1630,7 +1637,8 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     // Copy symbol table converting embedded EBCDIC names to ASCII.
     StringRef EbcdicSymbolTable = BufOrErr.get();
     if (EbcdicSymbolTable.size() < sizeof(uint32_t)) {
-      Err = malformedError("z/OS symbol table is too small to read the symbol count");
+      Err = malformedError(
+          "z/OS symbol table is too small to read the symbol count");
       return;
     }
     uint64_t EbcdicSymbolCount = read32be(EbcdicSymbolTable.data());
@@ -1654,7 +1662,7 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     if (Err)
       return;
     C = &*I;
-    
+
     setFirstRegular(*C);
     Err = Error::success();
     return;

>From d18485af7108dec5bb88e4e140364caa0beb5386 Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Tue, 21 Apr 2026 20:03:30 -0400
Subject: [PATCH 10/15] Replace canned binaries with python helper; increase
 test coverage

---
 llvm/lib/Object/Archive.cpp                   |   3 +-
 .../Object/Inputs/generate_zos_archive.py     | 352 ++++++++++++++++++
 llvm/test/Object/Inputs/zos-archive-test.a    | Bin 1424 -> 0 bytes
 llvm/test/Object/Inputs/zos-expected-foo.o    | Bin 1280 -> 0 bytes
 llvm/test/Object/zos-archive-read.test        |  52 ++-
 5 files changed, 396 insertions(+), 11 deletions(-)
 create mode 100644 llvm/test/Object/Inputs/generate_zos_archive.py
 delete mode 100644 llvm/test/Object/Inputs/zos-archive-test.a
 delete mode 100644 llvm/test/Object/Inputs/zos-expected-foo.o

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index b9f0674423000..6d1323d5aa0a2 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -105,6 +105,7 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent,
     *Err = createMemberHeaderParseError(this, RawHeaderPtr, Size);
     return;
   }
+  // '\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' &&
@@ -1626,8 +1627,6 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
   StringRef Name = NameOrErr.get();
 
   if (Name == "__.SYMDEF") {
-    // We know that the symbol table is not an external file, but we still must
-    // check any Expected<> return value.
     Expected<StringRef> BufOrErr = C->getBuffer();
     if (!BufOrErr) {
       Err = BufOrErr.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..a3866d23aa3e4
--- /dev/null
+++ b/llvm/test/Object/Inputs/generate_zos_archive.py
@@ -0,0 +1,352 @@
+#!/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
+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,
+)
+
+
+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: "!<arch>\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:<hexstring>   - decode hex bytes
+      <ascii string>    - 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/Inputs/zos-archive-test.a b/llvm/test/Object/Inputs/zos-archive-test.a
deleted file mode 100644
index 0244f176c64478cec3918b1ca6bed78b897eb2c1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1424
zcmbtUPfO!K6n}C0?;uOvZJ{h8rFa%ULOkuEMS4&O;=wPFy^9_q3yVkbQ1K`T{Twyz
zQZEwNMe!n`dT>uWjcN2`_q~}U9b at CN_~6a^|C{$_CfR809e>;ZS~ku4;Qm*)_d}P&
zLrFsF>Z<F*AWGOS1AvCSM*O|i0_1ut3di-*p`rx7FPuZ~Dco+m%s8U_AJN{a#AM{n
zoYZ1~EioBD7H8VAF%j_yYX&)q*;jEiOSM9sK>;8`M$R_v%WhBL5f-&cGL at 2>Q6d)2
zr1VFdOe=};nH8%Og=yFywE_j(+}*vp>Ue08Ba18=r(j5!J0Vt`=!&L+Ch&6g=F!of
zC%~w$9CEV1no<YT&#zbjOX8iG=iTd{0+0I2Pr*~+UTr3V49`zaRj_-#!ENXI7SZV*
zXtwA+mR|scMzVQwi(1hhF!~OJZ%&8^OBlHH8PsH^^i-mN$Gw0*3?GMqKk|J3z>#pg
z{1?tk8aBy3QKp5wi7xHn&Y3k_r?CRAwGMWvy$GcyxQ)Z23r)w(RBjC3@!L<J8IF^|
z%vnQs7PAWmx92OX*+0NO5?<RM-+#V1$&JTjhisx}Dd2#l4n<s-Fn$`krHJQ at yo~bX
zdx=~mU%VAH at Gl+lqW7Yv*6^?wM!{(G`*J{k7(A!M$#FG(qO&UM%XGQ&;>GKx+sSOv
Xf{c`};-bXq^`{Wi<G_bldx!EDV(Pt2

diff --git a/llvm/test/Object/Inputs/zos-expected-foo.o b/llvm/test/Object/Inputs/zos-expected-foo.o
deleted file mode 100644
index 4caf23315e4c9f140c3a0c11b92af7e0dfc77c7c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1280
zcmbtSy-EW?5T4EE=L(UiDT0WEU at bnvSy?EOM!3QvFOVnD;%uzNLTf?DbHp>CjT{I{
z8abkcR<4)3Xl0z)y`S|=BI3aA{D1rH?r;Hr6*jGbtK`(0Dg8}QorS}blRXCu5sxA{
zTv^O39f#Sv7ug&PfIK-xr{&yTb|oHR5lzw)C3_eZVpDDcKRRSwOM=hs#7qs&kRMS2
z!)fjB-`{t9^e9lkC5clq60A*y*HT-v7%+)f+-#hl9{3WBcolJF`PH>P82^4{18jwN
z(Yn6u-AX*-RT_h*!~Hl+CON*Jx|QK{yZy&b?-9}IALwrJf2 at 52ikvKY3d>$e4>-<?
ztmJc2#aPn8?TSNPRjNoePWZwfiox6a+b{_IKnw&5UazzvM1>(qzCe{Wu3ZeVqbGO9
z^4zU;@a!FMD(w}hG{D<BE_={$y?ph-5}ly^0nBpUJXX$Frn^#DwuG}(-7dTWpC`O_
zFnV6RIV+AvBbR)VPo?09v<YS0S22Gcg_TO~8AmzQsd<S*q`%FIp7_m1ym(&p(mFo&
zaySgbm%F}cn)FVYmlJw<YV#V}YxKG5<K=YI at 8p}b;X*0ZaMSWM{S$<nNn$p!{$YFp
DKWm9X

diff --git a/llvm/test/Object/zos-archive-read.test b/llvm/test/Object/zos-archive-read.test
index 2afcb1b79ee22..d9ba95c211c9e 100644
--- a/llvm/test/Object/zos-archive-read.test
+++ b/llvm/test/Object/zos-archive-read.test
@@ -1,13 +1,47 @@
-## Test reading a z/OS archive.
+## Test reading a valid z/OS archive.
+# RUN: %python %p/Inputs/generate_zos_archive.py --output %t.valid.a \
+# RUN:   --symtab "foo.txt: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
 
-# RUN: llvm-ar t %p/Inputs/zos-archive-test.a | FileCheck %s --check-prefix=LIST
-# RUN: llvm-nm --print-armap %p/Inputs/zos-archive-test.a | FileCheck %s --check-prefix=SYMS
+# LIST: foo.txt
+# SYMS: Archive map
+# SYMS-NEXT: foo.txt in foo.txt
+# CONTENT: abcd
 
-## Test extracting a member and verifying its content.
-# RUN: llvm-ar p %p/Inputs/zos-archive-test.a foo.o > %t.foo.o
-# RUN: cmp %t.foo.o %p/Inputs/zos-expected-foo.o
+## 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
 
-# LIST: foo.o
+## Test empty UID field on member header
+# 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
 
-# SYMS: Archive map
-# SYMS-NEXT: foo in foo.o
+## 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 symbol table is too small to read the symbol count
+
+## 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 symbol table count exceeds buffer size
+
+## 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
\ No newline at end of file

>From 967636eac9fba232f09318ccc7094d578e02e63d Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Tue, 21 Apr 2026 20:06:35 -0400
Subject: [PATCH 11/15] Run clang format

---
 llvm/lib/Object/Archive.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 6d1323d5aa0a2..a7a72e01a6526 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -105,7 +105,8 @@ ArchiveMemberHeader::ArchiveMemberHeader(const Archive *Parent,
     *Err = createMemberHeaderParseError(this, RawHeaderPtr, Size);
     return;
   }
-  // '\x79\x15' is the EBCDIC equivalent of '`\n' for the z/OS archive terminator.
+  // '\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' &&

>From c44535da97cdbdd0d98ecfcdc03637f8512559c5 Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Tue, 21 Apr 2026 20:19:01 -0400
Subject: [PATCH 12/15] run python formatter

---
 .../Object/Inputs/generate_zos_archive.py     | 204 +++++++++++-------
 1 file changed, 126 insertions(+), 78 deletions(-)

diff --git a/llvm/test/Object/Inputs/generate_zos_archive.py b/llvm/test/Object/Inputs/generate_zos_archive.py
index a3866d23aa3e4..124187d1fa8b4 100644
--- a/llvm/test/Object/Inputs/generate_zos_archive.py
+++ b/llvm/test/Object/Inputs/generate_zos_archive.py
@@ -31,6 +31,7 @@
 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,
@@ -41,35 +42,46 @@
     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')
+        s = s.encode("ascii")
     return bytes(ASCII_TO_EBCDIC_TABLE[b] for b in s)
 
 
-def ebcdic_pad(s, width, pad_char=' '):
+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: "!<arch>\n" in EBCDIC
-ZOS_MAGIC = b'\x5A\x4C\x81\x99\x83\x88\x6E\x15'
+ZOS_MAGIC = b"\x5a\x4c\x81\x99\x83\x88\x6e\x15"
 
 # Terminator: "`\n" in EBCDIC
-ZOS_TERMINATOR = b'\x79\x15'
+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):
+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):
@@ -83,7 +95,7 @@ def make_member_header(name, modtime, uid, gid, mode, size,
     Total: 60 bytes
     """
     # Handle long names
-    long_name_ext = b''
+    long_name_ext = b""
     if len(name) > 16:
         name_ebcdic = ascii_to_ebcdic(name)
         ext_len = len(name_ebcdic)
@@ -95,34 +107,34 @@ def make_member_header(name, modtime, uid, gid, mode, size,
         display_name = name
 
     if empty_name:
-        hdr = ebcdic_pad(' ', 16)
+        hdr = ebcdic_pad(" ", 16)
     else:
         hdr = ebcdic_pad(display_name, 16)
 
     if empty_modtime:
-        hdr += ebcdic_pad('', 12)
+        hdr += ebcdic_pad("", 12)
     else:
         hdr += ebcdic_pad(str(modtime), 12)
 
     if empty_uid:
-        hdr += ebcdic_pad('', 6)
+        hdr += ebcdic_pad("", 6)
     else:
         hdr += ebcdic_pad(str(uid), 6)
 
     if empty_gid:
-        hdr += ebcdic_pad('', 6)
+        hdr += ebcdic_pad("", 6)
     else:
         hdr += ebcdic_pad(str(gid), 6)
 
     if empty_mode:
-        hdr += ebcdic_pad('', 8)
+        hdr += ebcdic_pad("", 8)
     else:
         hdr += ebcdic_pad(str(mode), 8)
 
     hdr += ebcdic_pad(str(size), 10)
 
     if bad_terminator:
-        hdr += b'\x00\x00'
+        hdr += b"\x00\x00"
     else:
         hdr += ZOS_TERMINATOR
 
@@ -144,9 +156,9 @@ def make_symtab(symbols, member_offsets, truncated=False, bad_count=False):
     num_syms = len(symbols)
     if bad_count:
         # Write a count that exceeds the buffer.
-        body = struct.pack('>I', 0xFFFFFFFF)
+        body = struct.pack(">I", 0xFFFFFFFF)
     else:
-        body = struct.pack('>I', num_syms)
+        body = struct.pack(">I", num_syms)
 
     if truncated:
         # Return just the count, truncated before offset table.
@@ -154,10 +166,10 @@ def make_symtab(symbols, member_offsets, truncated=False, bad_count=False):
 
     for sym_name, mem_idx, attrs in symbols:
         offset = member_offsets[mem_idx]
-        body += struct.pack('>II', offset, attrs)
+        body += struct.pack(">II", offset, attrs)
 
     for sym_name, mem_idx, attrs in symbols:
-        body += ascii_to_ebcdic(sym_name) + b'\x00'
+        body += ascii_to_ebcdic(sym_name) + b"\x00"
 
     return body
 
@@ -171,22 +183,22 @@ def parse_member_data(raw):
       <ascii string>    - encode as raw ASCII bytes
     """
     if os.path.isfile(raw):
-        with open(raw, 'rb') as f:
+        with open(raw, "rb") as f:
             return f.read()
-    if raw.startswith('hex:'):
+    if raw.startswith("hex:"):
         return bytes.fromhex(raw[4:])
-    return raw.encode('ascii')
+    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',
+    "bad-terminator": "bad_terminator",
+    "empty-name": "empty_name",
+    "empty-uid": "empty_uid",
+    "empty-gid": "empty_gid",
+    "empty-modtime": "empty_modtime",
+    "empty-mode": "empty_mode",
 }
 
 
@@ -202,19 +214,19 @@ def build_archive(args):
     members = []
     if args.member:
         for m in args.member:
-            parts = m.split(':', 1)
+            parts = m.split(":", 1)
             name = parts[0]
             if len(parts) > 1:
                 data = parse_member_data(parts[1])
             else:
-                data = b'\x00' * 16  # Dummy content
+                data = b"\x00" * 16  # Dummy content
             members.append((name, data))
 
     # Parse symbols
     symbols = []
     if args.symtab:
         for s in args.symtab:
-            parts = s.split(':')
+            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
@@ -225,8 +237,10 @@ def build_archive(args):
     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())}")
+            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
@@ -235,8 +249,12 @@ def build_archive(args):
 
     # 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)
+    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.
@@ -247,7 +265,7 @@ def build_archive(args):
         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
+            symtab_body = struct.pack(">I", 0)  # 0 symbols
         else:
             placeholder_offsets = [0] * (len(members) + 1)
             symtab_body = make_symtab(symbols, placeholder_offsets)
@@ -279,8 +297,8 @@ def build_archive(args):
     # 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)
+            "__.SYMDEF", 0, 0, 0, 0, len(symtab_body), **symtab_hdr_kwargs
+        )
         output += symtab_hdr
         output += symtab_body
         # Pad to even boundary
@@ -289,7 +307,12 @@ def build_archive(args):
 
     for i, (name, data) in enumerate(members):
         hdr = make_member_header(
-            name, 1234567890, 0, 0, 100644, len(data),
+            name,
+            1234567890,
+            0,
+            0,
+            100644,
+            len(data),
             bad_terminator=args.bad_terminator,
             empty_name=args.empty_name,
             empty_uid=args.empty_uid,
@@ -307,46 +330,71 @@ def build_archive(args):
 
 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')
+        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:
+    with open(args.output, "wb") as f:
         f.write(data)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()

>From ef764e2ce931d413d3cdc5d8e364d8799aeba6ad Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Tue, 5 May 2026 22:36:06 -0400
Subject: [PATCH 13/15] Add more test coverage, address review comments

---
 llvm/lib/Object/Archive.cpp                   |  8 +++-
 .../Object/Inputs/generate_zos_archive.py     | 34 +++++++--------
 llvm/test/Object/zos-archive-read.test        | 42 ++++++++++++++-----
 3 files changed, 55 insertions(+), 29 deletions(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index a7a72e01a6526..ed0f6e2dec972 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -1638,14 +1638,18 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     StringRef EbcdicSymbolTable = BufOrErr.get();
     if (EbcdicSymbolTable.size() < sizeof(uint32_t)) {
       Err = malformedError(
-          "z/OS symbol table is too small to read the symbol count");
+          "z/OS archive symbol table is too small to read the symbol count, "
+          "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 symbol table count exceeds buffer 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;
diff --git a/llvm/test/Object/Inputs/generate_zos_archive.py b/llvm/test/Object/Inputs/generate_zos_archive.py
index 124187d1fa8b4..5489612f2633e 100644
--- a/llvm/test/Object/Inputs/generate_zos_archive.py
+++ b/llvm/test/Object/Inputs/generate_zos_archive.py
@@ -30,7 +30,7 @@
 import sys
 import os
 
-# EBCDIC / ASCII conversion table
+# 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,
@@ -58,13 +58,13 @@ def ebcdic_pad(s, width, pad_char=" "):
     return ascii_to_ebcdic(ascii_padded)
 
 
-# z/OS archive magic: "!<arch>\n" in EBCDIC
+# z/OS archive magic: "!<arch>\n" in EBCDIC.
 ZOS_MAGIC = b"\x5a\x4c\x81\x99\x83\x88\x6e\x15"
 
-# Terminator: "`\n" in EBCDIC
+# Terminator: "`\n" in EBCDIC.
 ZOS_TERMINATOR = b"\x79\x15"
 
-# EBCDIC newline for padding
+# EBCDIC newline for padding.
 EBCDIC_NEWLINE = b"\x15"
 
 
@@ -94,7 +94,7 @@ def make_member_header(
       ar_fmag:   2 bytes (terminator)
     Total: 60 bytes
     """
-    # Handle long names
+    # Handle long names.
     long_name_ext = b""
     if len(name) > 16:
         name_ebcdic = ascii_to_ebcdic(name)
@@ -210,7 +210,7 @@ def build_archive(args):
     if args.empty:
         return bytes(output)
 
-    # Parse members
+    # Parse members.
     members = []
     if args.member:
         for m in args.member:
@@ -219,10 +219,10 @@ def build_archive(args):
             if len(parts) > 1:
                 data = parse_member_data(parts[1])
             else:
-                data = b"\x00" * 16  # Dummy content
+                data = b"\x00" * 16  # Dummy content.
             members.append((name, data))
 
-    # Parse symbols
+    # Parse symbols.
     symbols = []
     if args.symtab:
         for s in args.symtab:
@@ -243,11 +243,11 @@ def build_archive(args):
             )
         symtab_hdr_kwargs[_SYMTAB_HDR_MALFORMATIONS[key]] = True
 
-    # Phase 1: Compute member offsets
-    # Start after magic
+    # Phase 1: Compute member offsets.
+    # Start after magic.
     pos = len(ZOS_MAGIC)
 
-    # If we have a symbol table, it comes first
+    # If we have a symbol table, it comes first.
     symtab_body = None
     has_symtab = (
         symbols
@@ -265,19 +265,19 @@ def build_archive(args):
         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
+            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_hdr_size = 60  # Fixed header for __.SYMDEF.
         symtab_total = symtab_hdr_size + len(symtab_body)
-        # Padding to even boundary
+        # Padding to even boundary.
         if symtab_total % 2 != 0:
             symtab_total += 1
         pos += symtab_total
 
-    # Compute member offsets
+    # Compute member offsets.
     member_offsets = []
     for name, data in members:
         member_offsets.append(pos)
@@ -294,14 +294,14 @@ def build_archive(args):
     if symbols and not args.symtab_truncated and not args.symtab_bad_count:
         symtab_body = make_symtab(symbols, member_offsets)
 
-    # Phase 2: Write output
+    # 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
+        # Pad to even boundary.
         if len(output) % 2 != 0:
             output += EBCDIC_NEWLINE
 
diff --git a/llvm/test/Object/zos-archive-read.test b/llvm/test/Object/zos-archive-read.test
index d9ba95c211c9e..c6b52740bb5c3 100644
--- a/llvm/test/Object/zos-archive-read.test
+++ b/llvm/test/Object/zos-archive-read.test
@@ -10,38 +10,60 @@
 # SYMS-NEXT: foo.txt in foo.txt
 # CONTENT: abcd
 
-## Test malformed terminator on member header
+## 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
+## 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
+# ERR-UID: UID field is empty or contains only spaces in archive member header at offset 8
 
-## Test leading space in member name
+## 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
+## 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 symbol table is too small to read the symbol count
+# ERR-TRUNC: z/OS archive symbol table is too small to read the symbol count, size is 2
 
-## Test z/OS symbol table where count exceeds buffer
+## 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 symbol table count exceeds buffer size
+# ERR-COUNT: z/OS archive symbol table names offset {{[0-9]+}} exceeds symbol table size 4
 
-## Test malformed __.SYMDEF header
+## 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
\ No newline at end of file
+# 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

>From c2241247f621f065b7175e5715a02bcf6e32dedd Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Tue, 5 May 2026 22:39:23 -0400
Subject: [PATCH 14/15] run clang format

---
 llvm/lib/Object/Archive.cpp | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index ed0f6e2dec972..9554ba5deeac7 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -1639,7 +1639,8 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
     if (EbcdicSymbolTable.size() < sizeof(uint32_t)) {
       Err = malformedError(
           "z/OS archive symbol table is too small to read the symbol count, "
-          "size is " + Twine(EbcdicSymbolTable.size()));       
+          "size is " +
+          Twine(EbcdicSymbolTable.size()));
       return;
     }
     uint64_t EbcdicSymbolCount = read32be(EbcdicSymbolTable.data());
@@ -1647,9 +1648,9 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
         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()));
+                           Twine(OffsetToEbcdicNames) +
+                           " exceeds symbol table size " +
+                           Twine(EbcdicSymbolTable.size()));
       return;
     }
     uint64_t EbcdicNamesSize = EbcdicSymbolTable.size() - OffsetToEbcdicNames;

>From 92d74f75265ab3258fd8c78da341bb302c1004dc Mon Sep 17 00:00:00 2001
From: Uyiosa Iyekekpolor <uyiosaben at gmail.com>
Date: Wed, 6 May 2026 22:22:08 -0400
Subject: [PATCH 15/15] Address review comments

---
 llvm/lib/Object/Archive.cpp            | 22 ++++++++++------------
 llvm/test/Object/zos-archive-read.test |  3 ++-
 2 files changed, 12 insertions(+), 13 deletions(-)

diff --git a/llvm/lib/Object/Archive.cpp b/llvm/lib/Object/Archive.cpp
index 9554ba5deeac7..bf09240ac2fbd 100644
--- a/llvm/lib/Object/Archive.cpp
+++ b/llvm/lib/Object/Archive.cpp
@@ -405,6 +405,7 @@ Expected<StringRef> ZOSArchiveMemberHeader::getRawName() const {
 }
 
 Expected<StringRef> ZOSArchiveMemberHeader::getName(uint64_t Size) const {
+  (void)Size;
   return StringRef(MemberName);
 }
 
@@ -433,7 +434,7 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
                           Twine(Offset));
     return;
   }
-  RawMemberName.append(RawNameRef);
+  RawMemberName.assign(RawNameRef);
 
   // Set MemberName.
   if (RawNameRef.starts_with("#1/")) {
@@ -459,7 +460,7 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
                        Twine(Offset));
     return;
   }
-  LastModified.append(LastModifiedRef);
+  LastModified.assign(LastModifiedRef);
 
   // UID
   StringRef UIDRef = ebcdicFieldToASCII(ArMemHdr->UID, Dst);
@@ -469,7 +470,7 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
                           Twine(Offset));
     return;
   }
-  UID.append(UIDRef);
+  UID.assign(UIDRef);
 
   // GID
   StringRef GIDRef = ebcdicFieldToASCII(ArMemHdr->GID, Dst);
@@ -479,7 +480,7 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
                           Twine(Offset));
     return;
   }
-  GID.append(GIDRef);
+  GID.assign(GIDRef);
 
   // AccessMode
   StringRef AccessModeRef = ebcdicFieldToASCII(ArMemHdr->AccessMode, Dst);
@@ -490,7 +491,7 @@ void ZOSArchiveMemberHeader::setMemberHeaderStrings(Error *Err, uint64_t Size) {
                        Twine(Offset));
     return;
   }
-  AccessMode.append(AccessModeRef);
+  AccessMode.assign(AccessModeRef);
 }
 
 Expected<uint64_t> BigArchiveMemberHeader::getRawNameSize() const {
@@ -1628,14 +1629,11 @@ ZOSArchive::ZOSArchive(MemoryBufferRef Source, Error &Err)
   StringRef Name = NameOrErr.get();
 
   if (Name == "__.SYMDEF") {
-    Expected<StringRef> BufOrErr = C->getBuffer();
-    if (!BufOrErr) {
-      Err = BufOrErr.takeError();
-      return;
-    }
-
     // Copy symbol table converting embedded EBCDIC names to ASCII.
-    StringRef EbcdicSymbolTable = BufOrErr.get();
+    // 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, "
diff --git a/llvm/test/Object/zos-archive-read.test b/llvm/test/Object/zos-archive-read.test
index c6b52740bb5c3..ba58cb990421f 100644
--- a/llvm/test/Object/zos-archive-read.test
+++ b/llvm/test/Object/zos-archive-read.test
@@ -1,6 +1,6 @@
 ## Test reading a valid z/OS archive.
 # RUN: %python %p/Inputs/generate_zos_archive.py --output %t.valid.a \
-# RUN:   --symtab "foo.txt:0" --member foo.txt:abcd
+# 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
@@ -8,6 +8,7 @@
 # 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.



More information about the llvm-commits mailing list