From 5f11a63d37253b2e8ec8592edbcf11ba6c6fe0f5 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Wed, 17 Nov 2010 20:40:35 +0000 Subject: [PATCH] allow white spaces and unicode in OPC relationship targets, see Bugzilla 50154 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1036215 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../opc/PackageRelationshipCollection.java | 12 +-- .../opc/PackageRelationshipTypes.java | 5 + .../poi/openxml4j/opc/PackagingURIHelper.java | 86 ++++++++++++++++-- .../marshallers/ZipPartMarshaller.java | 5 +- .../openxml4j/opc/TestPackagingURIHelper.java | 29 +++++- .../poi/openxml4j/opc/TestRelationships.java | 59 ++++++++++++ test-data/openxml4j/50154.xlsx | Bin 0 -> 13583 bytes 8 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 test-data/openxml4j/50154.xlsx diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 74eeae2ee7..be82f7f142 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 50154 - Allow white spaces and unicode in OPC relationship targets 50113 - Remove cell from Calculation Chain after setting cell type to blank 49966 - Ensure that XSSFRow#removeCell cleares calculation chain entries 50096 - Fixed evaluation of cell references with column index greater than 255 diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java index 990a3bf4cb..a5a40990c2 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java @@ -351,16 +351,8 @@ public final class PackageRelationshipCollection implements PackageRelationship.TARGET_ATTRIBUTE_NAME) .getValue(); - if (value.indexOf("\\") != -1) { - logger - .log(POILogger.INFO, "target contains \\ therefore not a valid URI" - + value + " replaced by /"); - value = value.replaceAll("\\\\", "/"); - // word can save external relationship with a \ instead - // of / - } - - target = new URI(value); + target = PackagingURIHelper.toURI(value); + } catch (URISyntaxException e) { logger.log(POILogger.ERROR, "Cannot convert " + value + " in a valid relationship URI-> ignored", e); diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipTypes.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipTypes.java index 337dacb4e1..01ed54c965 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipTypes.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipTypes.java @@ -75,6 +75,11 @@ public interface PackageRelationshipTypes { */ String IMAGE_PART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"; + /** + * Hyperlink type. + */ + String HYPERLINK_PART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"; + /** * Style type. */ diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackagingURIHelper.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackagingURIHelper.java index c4169d9dbf..4d48d2bd72 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackagingURIHelper.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackagingURIHelper.java @@ -19,6 +19,8 @@ package org.apache.poi.openxml4j.opc; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.io.UnsupportedEncodingException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.InvalidOperationException; @@ -287,7 +289,7 @@ public final class PackagingURIHelper { // form must actually be an absolute URI if(sourceURI.toString().equals("/")) { String path = targetURI.getPath(); - if(msCompatible && path.charAt(0) == '/') { + if(msCompatible && path.length() > 0 && path.charAt(0) == '/') { try { targetURI = new URI(path.substring(1)); } catch (Exception e) { @@ -362,6 +364,12 @@ public final class PackagingURIHelper { } } + // if the target had a fragment then append it to the result + String fragment = targetURI.getRawFragment(); + if (fragment != null) { + retVal.append("#").append(fragment); + } + try { return new URI(retVal.toString()); } catch (Exception e) { @@ -412,9 +420,9 @@ public final class PackagingURIHelper { * Get URI from a string path. */ public static URI getURIFromPath(String path) { - URI retUri = null; + URI retUri; try { - retUri = new URI(path); + retUri = toURI(path); } catch (URISyntaxException e) { throw new IllegalArgumentException("path"); } @@ -484,7 +492,7 @@ public final class PackagingURIHelper { throws InvalidFormatException { URI partNameURI; try { - partNameURI = new URI(resolvePartName(partName)); + partNameURI = toURI(partName); } catch (URISyntaxException e) { throw new InvalidFormatException(e.getMessage()); } @@ -648,7 +656,9 @@ public final class PackagingURIHelper { } /** - * If part name is not a valid URI, it is resolved as follows: + * Convert a string to {@link java.net.URI} + * + * If part name is not a valid URI, it is resolved as follows: *

* 1. Percent-encode each open bracket ([) and close bracket (]). * 2. Percent-encode each percent (%) character that is not followed by a hexadecimal notation of an octet value. @@ -663,12 +673,72 @@ public final class PackagingURIHelper { * in ?5.2 of RFC 3986. The path component of the resulting absolute URI is the part name. *

* - * @param partName the name to resolve + * @param value the string to be parsed into a URI * @return the resolved part name that should be OK to construct a URI * * TODO YK: for now this method does only (5). Finish the rest. */ - public static String resolvePartName(String partName){ - return partName.replace('\\', '/'); + public static URI toURI(String value) throws URISyntaxException { + //5. Convert all back slashes to forward slashes + if (value.indexOf("\\") != -1) { + value = value.replace('\\', '/'); + } + + // URI fragemnts (those starting with '#') are not encoded + // and may contain white spaces and raw unicode characters + int fragmentIdx = value.indexOf('#'); + if(fragmentIdx != -1){ + String path = value.substring(0, fragmentIdx); + String fragment = value.substring(fragmentIdx + 1); + + value = path + "#" + encode(fragment); + } + + return new URI(value); + } + + /** + * percent-encode white spaces and characters above 0x80. + *

+ * Examples: + * 'Apache POI' --> 'Apache%20POI' + * 'Apache\u0410POI' --> 'Apache%04%10POI' + * + * @param s the string to encode + * @return the encoded string + */ + public static String encode(String s) { + int n = s.length(); + if (n == 0) return s; + + ByteBuffer bb; + try { + bb = ByteBuffer.wrap(s.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e){ + // should not happen + throw new RuntimeException(e); + } + StringBuilder sb = new StringBuilder(); + while (bb.hasRemaining()) { + int b = bb.get() & 0xff; + if (isUnsafe(b)) { + sb.append('%'); + sb.append(hexDigits[(b >> 4) & 0x0F]); + sb.append(hexDigits[(b >> 0) & 0x0F]); + } else { + sb.append((char)b); + } + } + return sb.toString(); } + + private final static char[] hexDigits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + private static boolean isUnsafe(int ch) { + return ch > 0x80 || " ".indexOf(ch) >= 0; + } + } diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/marshallers/ZipPartMarshaller.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/marshallers/ZipPartMarshaller.java index c8bbb96af4..4a9fec855e 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/marshallers/ZipPartMarshaller.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/marshallers/ZipPartMarshaller.java @@ -163,10 +163,7 @@ public final class ZipPartMarshaller implements PartMarshaller { } else { URI targetURI = rel.getTargetURI(); targetValue = PackagingURIHelper.relativizeURI( - sourcePartURI, targetURI, true).getPath(); - if (targetURI.getRawFragment() != null) { - targetValue += "#" + targetURI.getRawFragment(); - } + sourcePartURI, targetURI, true).toString(); } relElem.addAttribute(PackageRelationship.TARGET_ATTRIBUTE_NAME, targetValue); diff --git a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackagingURIHelper.java b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackagingURIHelper.java index 7064fc48d8..9e2297d530 100644 --- a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackagingURIHelper.java +++ b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestPackagingURIHelper.java @@ -17,6 +17,7 @@ package org.apache.poi.openxml4j.opc; import java.net.URI; +import java.net.URISyntaxException; import junit.framework.TestCase; @@ -35,7 +36,9 @@ public class TestPackagingURIHelper extends TestCase { public void testRelativizeURI() throws Exception { URI uri1 = new URI("/word/document.xml"); URI uri2 = new URI("/word/media/image1.gif"); - + URI uri3 = new URI("/word/media/image1.gif#Sheet1!A1"); + URI uri4 = new URI("#'My%20Sheet1'!A1"); + // Document to image is down a directory URI retURI1to2 = PackagingURIHelper.relativizeURI(uri1, uri2); assertEquals("media/image1.gif", retURI1to2.getPath()); @@ -60,6 +63,12 @@ public class TestPackagingURIHelper extends TestCase { //URI compatible with MS Office and OpenOffice: leading slash is removed uriRes = PackagingURIHelper.relativizeURI(root, uri1, true); assertEquals("word/document.xml", uriRes.toString()); + + //preserve URI fragments + uriRes = PackagingURIHelper.relativizeURI(uri1, uri3, true); + assertEquals("media/image1.gif#Sheet1!A1", uriRes.toString()); + uriRes = PackagingURIHelper.relativizeURI(root, uri4, true); + assertEquals("#'My%20Sheet1'!A1", uriRes.toString()); } /** @@ -104,4 +113,22 @@ public class TestPackagingURIHelper extends TestCase { .equals(relativeName)); pkg.revert(); } + + public void testCreateURIFromString() throws Exception { + String[] href = { + "..\\\\\\cygwin\\home\\yegor\\.vim\\filetype.vim", + "..\\Program%20Files\\AGEIA%20Technologies\\v2.3.3\\NxCooking.dll", + "file:///D:\\seva\\1981\\r810102ns.mp3", + "..\\cygwin\\home\\yegor\\dinom\\%5baccess%5d.2010-10-26.log", + "#'Instructions (Text)'!B21" + }; + for(String s : href){ + try { + URI uri = PackagingURIHelper.toURI(s); + } catch (URISyntaxException e){ + fail("Failed to create URI from " + s); + } + } + } + } diff --git a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestRelationships.java b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestRelationships.java index 2b3b28138f..7711b1d0c8 100644 --- a/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestRelationships.java +++ b/src/ooxml/testcases/org/apache/poi/openxml4j/opc/TestRelationships.java @@ -18,6 +18,7 @@ package org.apache.poi.openxml4j.opc; import java.io.*; +import java.net.URI; import junit.framework.TestCase; @@ -254,4 +255,62 @@ public class TestRelationships extends TestCase { pkg.getRelationshipsByType("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties").getRelationship(0).getTargetURI().toString()); } + + public void testTargetWithSpecialChars() throws Exception{ + + OPCPackage pkg; + + String filepath = OpenXML4JTestDataSamples.getSampleFileName("50154.xlsx"); + pkg = OPCPackage.open(filepath); + assert_50154(pkg); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + pkg.save(baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + pkg = OPCPackage.open(bais); + + assert_50154(pkg); + } + + public void assert_50154(OPCPackage pkg) throws Exception { + URI drawingURI = new URI("/xl/drawings/drawing1.xml"); + PackagePart drawingPart = pkg.getPart(PackagingURIHelper.createPartName(drawingURI)); + PackageRelationshipCollection drawingRels = drawingPart.getRelationships(); + + assertEquals(6, drawingRels.size()); + + // expected one image + assertEquals(1, drawingPart.getRelationshipsByType("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image").size()); + // and three hyperlinks + assertEquals(5, drawingPart.getRelationshipsByType("http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink").size()); + + PackageRelationship rId1 = drawingPart.getRelationship("rId1"); + URI parent = drawingPart.getPartName().getURI(); + URI rel1 = parent.relativize(rId1.getTargetURI()); + URI rel11 = PackagingURIHelper.relativizeURI(drawingPart.getPartName().getURI(), rId1.getTargetURI()); + assertEquals("'Another Sheet'!A1", rel1.getFragment()); + + PackageRelationship rId2 = drawingPart.getRelationship("rId2"); + URI rel2 = PackagingURIHelper.relativizeURI(drawingPart.getPartName().getURI(), rId2.getTargetURI()); + assertEquals("../media/image1.png", rel2.getPath()); + + PackageRelationship rId3 = drawingPart.getRelationship("rId3"); + URI rel3 = parent.relativize(rId3.getTargetURI()); + assertEquals("ThirdSheet!A1", rel3.getFragment()); + + PackageRelationship rId4 = drawingPart.getRelationship("rId4"); + URI rel4 = parent.relativize(rId4.getTargetURI()); + assertEquals("'\u0410\u043F\u0430\u0447\u0435 \u041F\u041E\u0418'!A1", rel4.getFragment()); + + PackageRelationship rId5 = drawingPart.getRelationship("rId5"); + URI rel5 = parent.relativize(rId5.getTargetURI()); + // back slashed have been replaced with forward + assertEquals("file:///D:/chan-chan.mp3", rel5.toString()); + + PackageRelationship rId6 = drawingPart.getRelationship("rId6"); + URI rel6 = parent.relativize(rId6.getTargetURI()); + assertEquals("../../../../../../../cygwin/home/yegor/dinom/&&&[access].2010-10-26.log", rel6.getPath()); + assertEquals("'\u0410\u043F\u0430\u0447\u0435 \u041F\u041E\u0418'!A5", rel6.getFragment()); + } + } diff --git a/test-data/openxml4j/50154.xlsx b/test-data/openxml4j/50154.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7637fb37ba23f05510abafbb744b16312ee74d3b GIT binary patch literal 13583 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