HyukjinKwon commented on code in PR #56784:
URL: https://github.com/apache/spark/pull/56784#discussion_r3478114506


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/ArchiveReader.scala:
##########
@@ -67,25 +106,109 @@ abstract class ArchiveReader(path: Path) {
   def readEntries[T](
       conf: Configuration,
       ignoredPathSegmentRegex: Pattern = 
HadoopFSUtils.defaultIgnoredPathSegmentRegexPattern)(
-      parseEntry: (String, InputStream) => Iterator[T]): Iterator[T]
+      parseEntry: (String, InputStream) => Iterator[T]): Iterator[T] = {
+    val archive = openArchiveStream(conf)
+    var closed = false
+
+    def cleanup(): Unit = {
+      if (!closed) {
+        closed = true
+        try archive.close() catch { case NonFatal(_) => }
+      }
+    }
+
+    Option(TaskContext.get()).foreach(_.addTaskCompletionListener[Unit](_ => 
cleanup()))
+
+    val entries = new Iterator[T] with Closeable {
+      private var currentIter: Iterator[T] = Iterator.empty
+      private var done = false
+
+      // Move to the next entry whose iterator has elements (releasing each 
exhausted entry's
+      // reader and skipping any unread bytes), or mark the stream done once 
entries run out.
+      // Advancing here -- driven by `hasNext` -- rather than eagerly after 
producing a row in
+      // `next` is essential for parsers that reuse a single mutable row and 
look ahead on
+      // `hasNext`: probing the current entry right after returning a row 
would overwrite that row's
+      // contents before the caller has copied it.
+      private def advance(): Unit = {
+        while (!done && !currentIter.hasNext) {
+          currentIter match {
+            case c: Closeable => try c.close() catch { case NonFatal(_) => }
+            case _ =>
+          }
+          var entry = archive.getNextEntry

Review Comment:
   The advance loop calls `parseEntry` on every non-skipped entry without 
consulting `ArchiveInputStream.canReadEntryData(entry)`. For 
`ZipArchiveInputStream` that returns false for entries it can't stream — a 
STORED entry with a trailing data descriptor, an unsupported compression 
method, or encryption — i.e. exactly the "a few unusual zips … are not 
streamable this way" case the new Scaladoc documents.
   
   Question: on such an entry, does the current path fail loudly with a clear 
error, or can it silently yield a truncated/garbled entry? If the latter is 
possible, consider guarding here:
   ```scala
   if (entry != null && !archive.canReadEntryData(entry)) {
     throw <clear error naming the unsupported zip feature>
   }
   ```
   so the documented limitation surfaces deterministically rather than 
depending on `ZipArchiveInputStream`'s default read behavior.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/ArchiveReader.scala:
##########
@@ -181,90 +286,19 @@ class TarArchiveReader(path: Path) extends 
ArchiveReader(path) {
         throw e
     }
   }
+}
 
-  /**
-   * Wraps the shared tar stream as a view over exactly the current entry's 
bytes
-   * (`TarArchiveInputStream.read` returns -1 at the entry boundary). 
[[CloseShieldInputStream]]
-   * ignores `close()`, so a parser closing its input does not close the 
underlying archive; any
-   * unread remainder of an entry is skipped by `getNextEntry()` when 
advancing.
-   */
-  private def entryStream(tar: TarArchiveInputStream): InputStream =
-    CloseShieldInputStream.wrap(tar)
-
-  override def readEntries[T](
-      conf: Configuration,
-      ignoredPathSegmentRegex: Pattern)(
-      parseEntry: (String, InputStream) => Iterator[T]): Iterator[T] = {
-    val tar = openTarStream(conf)
-    var closed = false
-
-    def cleanup(): Unit = {
-      if (!closed) {
-        closed = true
-        try tar.close() catch { case NonFatal(_) => }
-      }
-    }
-
-    Option(TaskContext.get()).foreach(_.addTaskCompletionListener[Unit](_ => 
cleanup()))
-
-    val entries = new Iterator[T] with Closeable {
-      private var currentIter: Iterator[T] = Iterator.empty
-      private var done = false
-
-      // Move to the next entry whose iterator has elements (releasing each 
exhausted entry's
-      // reader and skipping any unread bytes), or mark the stream done once 
entries run out.
-      // Advancing here -- driven by `hasNext` -- rather than eagerly after 
producing a row in
-      // `next` is essential for parsers that reuse a single mutable row and 
look ahead on
-      // `hasNext`: probing the current entry right after returning a row 
would overwrite that row's
-      // contents before the caller has copied it.
-      private def advance(): Unit = {
-        while (!done && !currentIter.hasNext) {
-          currentIter match {
-            case c: Closeable => try c.close() catch { case NonFatal(_) => }
-            case _ =>
-          }
-          var entry = tar.getNextEntry
-          while (entry != null && shouldSkipEntry(entry, 
ignoredPathSegmentRegex)) {
-            entry = tar.getNextEntry
-          }
-          if (entry == null) {
-            done = true
-            cleanup()
-          } else {
-            currentIter = parseEntry(entry.getName, entryStream(tar))
-          }
-        }
-      }
-
-      override def hasNext: Boolean = {
-        advance()
-        !done && currentIter.hasNext
-      }
-
-      override def next(): T = {
-        if (!hasNext) throw new NoSuchElementException
-        currentIter.next()
-      }
-
-      override def close(): Unit = {
-        done = true
-        currentIter = Iterator.empty
-        cleanup()
-      }
-    }
+/**
+ * [[ArchiveReader]] for zip archives (`.zip`). Each entry is individually 
compressed inside the
+ * container (the container itself is not gzip-wrapped), so 
`ZipArchiveInputStream` decompresses
+ * entries as they are streamed and no Hadoop codec layer is applied. The 
stream reads local file
+ * headers sequentially rather than the central directory, matching the tar 
reader's pure-streaming
+ * model: a few unusual zips (e.g. a deflated entry whose size is recorded 
only in a trailing data
+ * descriptor) are not streamable this way.
+ */
+class ZipArchiveReader(path: Path) extends ArchiveReader(path) {
 
-    // Open the first entry eagerly so the construction cost (and any failure) 
surfaces here rather
-    // than at the first `hasNext`. A corrupt archive throws before the caller 
ever holds the
-    // iterator, leaving it nothing to close: executors release the stream 
through the task-
-    // completion listener, but driver-side callers (e.g. Avro's header-only 
schema inference) have
-    // no task, so close it here before propagating.
-    try {
-      entries.hasNext
-    } catch {
-      case NonFatal(e) =>
-        cleanup()
-        throw e
-    }
-    entries
-  }
+  override protected def openArchiveStream(
+      conf: Configuration): ArchiveInputStream[_ <: ArchiveEntry] =
+    new 
ZipArchiveInputStream(CodecStreams.createInputStreamWithCloseResource(conf, 
path))

Review Comment:
   The "not streamable" limitation this `ZipArchiveReader` documents has no 
test pinning its behavior. `ZipArchiveTestUtils.writeArchive` uses 
`ZipArchiveOutputStream` (good — an independent producer, distinct from the 
reader) and there's a `writeCorruptArchive` negative case, but nothing 
exercises a *valid-but-non-streamable* zip. Consider adding a fixture (e.g. a 
STORED entry written with a data descriptor) that asserts the chosen behavior, 
so a future change can't silently turn the `canReadEntryData` gap above into 
corrupt rows.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to