This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new f988a65b4e TIKA-5050: resolve EPUB manifest hrefs as URI references 
(#3546)
f988a65b4e is described below

commit f988a65b4eff7eb84405364711e22bcbd2c07387
Author: Dominik Schmidt <[email protected]>
AuthorDate: Fri Nov 6 17:41:43 2026 +0100

    TIKA-5050: resolve EPUB manifest hrefs as URI references (#3546)
    
    * TIKA-5050: resolve EPUB manifest hrefs as URI references
    
    URLDecoder decodes form data and made a '+' a space, and the href was 
appended to the OPF's folder without resolving '../'. ZipEntryPaths 
(zip-commons) resolves a reference against the entry holding it per RFC 3986 
and percent-decodes it as a URI path; EpubParser uses it for manifest hrefs and 
encryption.xml URIs alike.
    
    * TIKA-5050: address review: literal base entry, no entry for a reference 
without a path
    
    The entry a reference sits in is a name, not a URI: a %, # or ? in it was 
interpreted, so My%20Book/content.opf resolved its chapters into My Book/. A 
reference that is empty or only a fragment or query resolved to the OPF or its 
folder; it now points at no entry.
    
    ---------
    
    Co-authored-by: Tim Allison <[email protected]>
---
 CHANGES.txt                                        |   5 +
 .../org/apache/tika/parser/epub/EpubParser.java    |  35 ++-----
 .../apache/tika/parser/epub/EpubParserTest.java    | 116 +++++++++++++++++++++
 .../org/apache/tika/zip/utils/ZipEntryPaths.java   |  73 +++++++++++++
 .../apache/tika/zip/utils/ZipEntryPathsTest.java   |  93 +++++++++++++++++
 5 files changed, 298 insertions(+), 24 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index fd37309e65..d96cda2cb8 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -81,6 +81,11 @@ Release 4.3.0 - ???
 
   EXTRACTION AND FORMATS
 
+   * EpubParser resolves manifest hrefs as URI references relative to the OPF: 
a '+' is
+     no longer read as a space, and '../' is followed, so such chapters and 
images are no
+     longer skipped. encryption.xml URIs are resolved the same way. The new
+     ZipEntryPaths in tika-parser-zip-commons does the resolving (TIKA-5050).
+     
    * Canon CR3 raws have a parser of their own, Cr3Parser in the image module, 
instead
      of going through the MP4 parser: their Exif data (IFD0, Exif IFD, Canon 
maker note,
      GPS) is extracted, the JPEG preview (PRVW, 1620x1080 on current bodies) 
is the
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/epub/EpubParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/epub/EpubParser.java
index ad0ad4ee90..6faf3dd212 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/epub/EpubParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/epub/EpubParser.java
@@ -20,8 +20,6 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -65,6 +63,7 @@ import org.apache.tika.sax.EmbeddedContentHandler;
 import org.apache.tika.sax.XHTMLBalancingHandler;
 import org.apache.tika.sax.XHTMLContentHandler;
 import org.apache.tika.utils.XMLReaderUtils;
+import org.apache.tika.zip.utils.ZipEntryPaths;
 import org.apache.tika.zip.utils.ZipFileHelper;
 
 /**
@@ -201,11 +200,6 @@ public class EpubParser implements Parser {
             return fallbackParseAllHtmlEntries(zipFile, bodyHandler, 
normalizer, metadata, context,
                     "OPF declared no spine items in (possibly truncated) 
container");
         }
-        String relativePath = "";
-        if (rootOPF.lastIndexOf("/") > -1) {
-            relativePath = rootOPF.substring(0, rootOPF.lastIndexOf("/") + 1);
-        }
-
         extractMetadata(zipFile, metadata, context);
         Set<String> encryptedItems = checkForDRM(zipFile);
         Set<String> processed = new HashSet<>();
@@ -228,12 +222,12 @@ public class EpubParser implements Parser {
                     shouldParse = true;
                 }
                 if (shouldParse) {
-                    String path = relativePath + hRefMediaPair.href;
+                    String path = ZipEntryPaths.resolve(rootOPF, 
hRefMediaPair.href);
                     //if content is encrypted, do not parse it, throw an 
exception now
                     if (encryptedItems.contains(path)) {
                         maybeThrowEncryptedException(encryptedItems);
                     }
-                    zae = zipFile.getEntry(relativePath + hRefMediaPair.href);
+                    zae = path == null ? null : zipFile.getEntry(path);
                     if (zae != null) {
                         try (TikaInputStream tis = entryStream(zipFile, zae)) {
                             content.parse(tis, bodyHandler, metadata, context);
@@ -278,12 +272,12 @@ public class EpubParser implements Parser {
         for (String id : contentOrderScraper.locationMap.keySet()) {
             if (!processed.contains(id)) {
                 HRefMediaPair hRefMediaPair = 
contentOrderScraper.locationMap.get(id);
-                String fullPath = relativePath + hRefMediaPair.href;
-                if (encryptedItems.contains(fullPath)) {
+                String fullPath = ZipEntryPaths.resolve(rootOPF, 
hRefMediaPair.href);
+                if (fullPath == null || encryptedItems.contains(fullPath)) {
                     continue;
                 }
                 if (shouldHandleEmbedded(hRefMediaPair.media)) {
-                    handleEmbedded(zipFile, relativePath, hRefMediaPair,
+                    handleEmbedded(zipFile, fullPath, hRefMediaPair,
                             id.equals(contentOrderScraper.getCoverId()),
                             embeddedDocumentExtractor, xhtml, metadata, 
context);
                 }
@@ -434,17 +428,12 @@ public class EpubParser implements Parser {
         return TikaInputStream.get(() -> zipFile.getInputStream(entry), new 
TemporaryResources(), null);
     }
 
-    private void handleEmbedded(ZipFile zipFile, String relativePath, 
HRefMediaPair hRefMediaPair,
+    private void handleEmbedded(ZipFile zipFile, String fullPath, 
HRefMediaPair hRefMediaPair,
                                 boolean cover,
                                 EmbeddedDocumentExtractor 
embeddedDocumentExtractor,
                                 XHTMLContentHandler xhtml, Metadata 
parentMetadata,
                                 ParseContext context)
             throws IOException, SAXException, TikaException {
-        if (hRefMediaPair.href == null) {
-            return;
-        }
-        String fullPath = relativePath + hRefMediaPair.href;
-
         ZipArchiveEntry ze = zipFile.getEntry(fullPath);
         if (ze == null || !zipFile.canReadEntryData(ze)) {
             return;
@@ -569,11 +558,6 @@ public class EpubParser implements Parser {
                     String href = XMLReaderUtils.getAttrValue("href", atts);
                     String mime = XMLReaderUtils.getAttrValue("media-type", 
atts);
                     if (id != null && href != null) {
-                        try {
-                            href = URLDecoder.decode(href, UTF_8.name());
-                        } catch (UnsupportedEncodingException e) {
-                            //swallow
-                        }
                         locationMap.put(id, new HRefMediaPair(href, mime));
                         String properties = 
XMLReaderUtils.getAttrValue("properties", atts);
                         if (coverImageItem == null && properties != null
@@ -640,7 +624,10 @@ public class EpubParser implements Parser {
         public void startElement(String uri, String localName, String qName, 
Attributes attributes) {
             if ("CipherReference".equals(localName)) {
                 String encryptedUri = XMLReaderUtils.getAttrValue("URI", 
attributes);
-                encryptedItems.add(encryptedUri);
+                if (encryptedUri != null) {
+                    String path = ZipEntryPaths.resolve("", encryptedUri);
+                    encryptedItems.add(path != null ? path : encryptedUri);
+                }
             }
         }
         public Set<String> getEncryptedItems() {
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/epub/EpubParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/epub/EpubParserTest.java
index 0e133b3a02..32a5d6ea02 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/epub/EpubParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/epub/EpubParserTest.java
@@ -21,10 +21,16 @@ import static 
org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -185,6 +191,116 @@ public class EpubParserTest extends TikaTest {
         assertEquals("test rights", m.getValues(TikaCoreProperties.RIGHTS)[1]);
     }
 
+    /**
+     * Manifest hrefs are URI references relative to the OPF: a '+' is a '+', 
percent
+     * escapes are decoded, '../' and fragments are resolved.
+     */
+    @Test
+    public void testHrefsAreResolvedAsUris() throws Exception {
+        String[][] items = {
+                {"a", "Text/plain.xhtml", "OEBPS/Text/plain.xhtml", 
"PLAINCHAPTER"},
+                {"b", "Text/c++.xhtml", "OEBPS/Text/c++.xhtml", "PLUSCHAPTER"},
+                {"c", "Text/My%20Chapter.xhtml", "OEBPS/Text/My 
Chapter.xhtml", "SPACECHAPTER"},
+                {"d", "../Shared/up.xhtml", "Shared/up.xhtml", "UPCHAPTER"},
+                {"e", "Text/frag.xhtml#start", "OEBPS/Text/frag.xhtml", 
"FRAGMENTCHAPTER"}};
+        List<Metadata> list;
+        try (TikaInputStream tis = TikaInputStream.get(epub(items, null))) {
+            list = getRecursiveMetadata(tis, true);
+        }
+        String content = list.get(0).get(TikaCoreProperties.TIKA_CONTENT);
+        for (String[] item : items) {
+            assertContains(item[3], content);
+        }
+        Metadata cover = list.get(1);
+        assertEquals("OEBPS/Images/cover art.png", 
cover.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+        
assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(),
+                cover.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+    }
+
+    /**
+     * encryption.xml names its items by URI too: an item named with an escape 
is still known
+     * as encrypted, so its bytes are neither parsed as a chapter nor emitted.
+     */
+    @Test
+    public void testEscapedEncryptedItems() throws Exception {
+        String[][] items = {{"a", "Text/My%20Chapter.xhtml", "OEBPS/Text/My 
Chapter.xhtml",
+                "SPACECHAPTER"}};
+        List<Metadata> list;
+        try (TikaInputStream tis = TikaInputStream.get(epub(items,
+                "OEBPS/Text/My%20Chapter.xhtml"))) {
+            list = getRecursiveMetadata(tis, true);
+        }
+        assertContains("EncryptedDocumentException",
+                list.get(0).get(TikaCoreProperties.CONTAINER_EXCEPTION));
+        assertNotContained("SPACECHAPTER",
+                
String.valueOf(list.get(0).get(TikaCoreProperties.TIKA_CONTENT)));
+
+        try (TikaInputStream tis = TikaInputStream.get(epub(items,
+                "OEBPS/Images/cover%20art.png"))) {
+            list = getRecursiveMetadata(tis, true);
+        }
+        assertContains("SPACECHAPTER", 
list.get(0).get(TikaCoreProperties.TIKA_CONTENT));
+        assertEquals(1, list.size(), "the encrypted cover is not emitted");
+    }
+
+    /**
+     * An EPUB with its OPF in OEBPS/, one spine item per {id, href, entry 
name, text}, a
+     * cover image whose href holds an escaped space, and, if given, an 
encrypted URI.
+     */
+    private static byte[] epub(String[][] items, String encryptedUri) throws 
IOException {
+        StringBuilder manifest = new StringBuilder();
+        StringBuilder spine = new StringBuilder();
+        for (String[] item : items) {
+            manifest.append("<item id=\"").append(item[0]).append("\" 
href=\"").append(item[1])
+                    .append("\" media-type=\"application/xhtml+xml\"/>");
+            spine.append("<itemref idref=\"").append(item[0]).append("\"/>");
+        }
+        manifest.append("<item id=\"cover\" href=\"Images/cover%20art.png\" "
+                + "media-type=\"image/png\" properties=\"cover-image\"/>");
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        try (ZipOutputStream zip = new ZipOutputStream(bytes)) {
+            byte[] mimetype = 
"application/epub+zip".getBytes(StandardCharsets.US_ASCII);
+            ZipEntry first = new ZipEntry("mimetype");
+            first.setMethod(ZipEntry.STORED);
+            first.setSize(mimetype.length);
+            CRC32 crc = new CRC32();
+            crc.update(mimetype);
+            first.setCrc(crc.getValue());
+            zip.putNextEntry(first);
+            zip.write(mimetype);
+            put(zip, "META-INF/container.xml", "<?xml 
version=\"1.0\"?><container version=\"1.0\" "
+                    + 
"xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"><rootfiles>"
+                    + "<rootfile full-path=\"OEBPS/content.opf\" "
+                    + 
"media-type=\"application/oebps-package+xml\"/></rootfiles></container>");
+            if (encryptedUri != null) {
+                put(zip, "META-INF/encryption.xml", "<?xml 
version=\"1.0\"?><encryption "
+                        + 
"xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\" "
+                        + 
"xmlns:enc=\"http://www.w3.org/2001/04/xmlenc#\";><enc:EncryptedData>"
+                        + "<enc:CipherData><enc:CipherReference URI=\"" + 
encryptedUri + "\"/>"
+                        + 
"</enc:CipherData></enc:EncryptedData></encryption>");
+            }
+            put(zip, "OEBPS/content.opf", "<?xml version=\"1.0\"?><package "
+                    + "xmlns=\"http://www.idpf.org/2007/opf\"; version=\"3.0\" "
+                    + "unique-identifier=\"id\"><metadata "
+                    + 
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\";><dc:identifier id=\"id\">x"
+                    + 
"</dc:identifier><dc:title>T</dc:title></metadata><manifest>" + manifest
+                    + "</manifest><spine>" + spine + "</spine></package>");
+            for (String[] item : items) {
+                put(zip, item[2], "<?xml version=\"1.0\"?><html "
+                        + 
"xmlns=\"http://www.w3.org/1999/xhtml\";><head><title>t</title></head>"
+                        + "<body><p>" + item[3] + "</p></body></html>");
+            }
+            zip.putNextEntry(new ZipEntry("OEBPS/Images/cover art.png"));
+            zip.write(new byte[]{(byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, 
'\n'});
+        }
+        return bytes.toByteArray();
+    }
+
+    private static void put(ZipOutputStream zip, String name, String content) 
throws IOException {
+        zip.putNextEntry(new ZipEntry(name));
+        zip.write(content.getBytes(StandardCharsets.UTF_8));
+    }
+
     private Set<String> set(Metadata m, Property property) {
         return new HashSet<>(Arrays.asList(m.getValues(property)));
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/zip/utils/ZipEntryPaths.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/zip/utils/ZipEntryPaths.java
new file mode 100644
index 0000000000..090406f498
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/zip/utils/ZipEntryPaths.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.zip.utils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Resolves the URI references one entry of a zip package makes to another, as 
an EPUB
+ * manifest's hrefs or an OPC package's relationship targets.
+ */
+public final class ZipEntryPaths {
+
+    private ZipEntryPaths() {
+    }
+
+    /**
+     * The name of the entry a reference points to: the reference resolved 
against the entry
+     * holding it (RFC 3986, section 5.2) and percent-decoded as a URI path, 
without the
+     * leading slash. A {@code +} stays a {@code +}. A reference that is not a 
valid URI, as
+     * one with a raw space, is taken as a literal path. The entry name it is 
resolved against
+     * is literal: a {@code %}, {@code #} or {@code ?} in it is part of the 
name.
+     *
+     * @param from      the name of the entry holding the reference, "" for 
the package root
+     * @param reference the URI reference
+     * @return the entry name, or {@code null} for a reference with a scheme 
or an
+     * authority, one without a path (empty or only a fragment), or one that 
leads above the
+     * package root
+     */
+    public static String resolve(String from, String reference) {
+        if (from == null || reference == null) {
+            return null;
+        }
+        try {
+            URI ref = uri(reference.trim());
+            if (ref.getScheme() != null || ref.getRawAuthority() != null
+                    || ref.getRawPath() == null || ref.getRawPath().isEmpty()) 
{
+                return null;
+            }
+            String base = from.startsWith("/") ? from : "/" + from;
+            String path = new URI(null, null, base, 
null).resolve(ref).normalize().getPath();
+            if (path == null || !path.startsWith("/") || path.equals("/..")
+                    || path.startsWith("/../")) {
+                return null;
+            }
+            return path.substring(1);
+        } catch (URISyntaxException | IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    private static URI uri(String s) throws URISyntaxException {
+        try {
+            return new URI(s);
+        } catch (URISyntaxException e) {
+            return new URI(null, null, s, null);
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/zip/utils/ZipEntryPathsTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/zip/utils/ZipEntryPathsTest.java
new file mode 100644
index 0000000000..62815a8e1b
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/zip/utils/ZipEntryPathsTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.zip.utils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+public class ZipEntryPathsTest {
+
+    @Test
+    public void testRelativeToTheEntry() {
+        assertEquals("OEBPS/Text/ch1.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "Text/ch1.xhtml"));
+        assertEquals("OEBPS/Text/ch1.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "./Text/ch1.xhtml"));
+        assertEquals("Images/cover.jpg", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "../Images/cover.jpg"));
+        assertEquals("ch1.xhtml", ZipEntryPaths.resolve("content.opf", 
"ch1.xhtml"));
+        assertEquals("Metadata/thumbnail.png", ZipEntryPaths.resolve("", 
"Metadata/thumbnail.png"));
+    }
+
+    @Test
+    public void testAbsolutePath() {
+        assertEquals("Metadata/thumbnail.png", ZipEntryPaths.resolve("",
+                "/Metadata/thumbnail.png"));
+        assertEquals("Thumbnails/box.png", 
ZipEntryPaths.resolve("3D/3dmodel.model",
+                "/Thumbnails/box.png"));
+    }
+
+    @Test
+    public void testDecoding() {
+        assertEquals("OEBPS/Text/c++ b.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "Text/c++%20b.xhtml"));
+        assertEquals("OEBPS/Text/Kapitel รค.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "Text/Kapitel%20%C3%A4.xhtml"));
+        assertEquals("OEBPS/Text/ch1.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "Text/ch1.xhtml#section2"));
+    }
+
+    @Test
+    public void testNotAValidUri() {
+        assertEquals("OEBPS/Text/my chapter.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "Text/my chapter.xhtml"));
+        assertEquals("OEBPS/Text/100%.xhtml", 
ZipEntryPaths.resolve("OEBPS/content.opf",
+                "Text/100%.xhtml"));
+        assertEquals("My Book/Text/ch1.xhtml", ZipEntryPaths.resolve("My 
Book/content.opf",
+                "Text/ch1.xhtml"));
+    }
+
+    /** The entry the reference sits in is a name, not a URI: its %, # and ? 
are literal. */
+    @Test
+    public void testLiteralBase() {
+        assertEquals("My%20Book/Text/ch1.xhtml", 
ZipEntryPaths.resolve("My%20Book/content.opf",
+                "Text/ch1.xhtml"));
+        assertEquals("OEBPS#1/Text/ch1.xhtml", 
ZipEntryPaths.resolve("OEBPS#1/content.opf",
+                "Text/ch1.xhtml"));
+        assertEquals("a?b/ch1.xhtml", ZipEntryPaths.resolve("a?b/content.opf", 
"ch1.xhtml"));
+    }
+
+    /** A reference without a path points at no other entry. */
+    @Test
+    public void testNoPath() {
+        assertNull(ZipEntryPaths.resolve("OEBPS/content.opf", ""));
+        assertNull(ZipEntryPaths.resolve("OEBPS/content.opf", "#frag"));
+        assertNull(ZipEntryPaths.resolve("OEBPS/content.opf", "?query"));
+    }
+
+    @Test
+    public void testOutsideThePackage() {
+        assertNull(ZipEntryPaths.resolve("OEBPS/content.opf", 
"../../etc/passwd"));
+        assertNull(ZipEntryPaths.resolve("", "../x"));
+        assertNull(ZipEntryPaths.resolve("", "http://example.com/x.png";));
+        assertNull(ZipEntryPaths.resolve("", "//example.com/x.png"));
+        assertNull(ZipEntryPaths.resolve("", "file:///etc/passwd"));
+        assertNull(ZipEntryPaths.resolve("", null));
+    }
+}

Reply via email to