Skip to content

Commit

Permalink
Merge pull request #1195 from tachyonicClock/bug_1194
Browse files Browse the repository at this point in the history
Fix `IllegalArgumentException` with non-ascii paths
  • Loading branch information
marscher authored Oct 2, 2024
2 parents a566bae + 67940bf commit 354968c
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 16 deletions.
4 changes: 4 additions & 0 deletions doc/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ Latest Changes:

- Support of np.float16 conversion with arrays.

- Fixed a problem that caused ``dir(jpype.JPackage("mypackage"))`` to fail if
the class path contained non-ascii characters. See issue #1194.

- Fixed ``BufferOverflowException`` in ``JUnpickler`` when decoding
multiple Java objects.


- **1.5.0 - 2023-04-03**

- Support for Python 3.12
Expand Down
37 changes: 26 additions & 11 deletions jpype/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import atexit
import os
from pathlib import Path
import sys
import typing

Expand All @@ -31,6 +32,8 @@

from ._jvmfinder import *

from jpype._classpath import addClassPath

# This import is required to bootstrap importlib, _jpype uses importlib.util
# but on some systems it may not load properly from C. To make sure it gets
# loaded properly we are going to force the issue even through we don't
Expand Down Expand Up @@ -115,12 +118,15 @@ def _hasClassPath(args) -> bool:
return False


def _handleClassPath(classpath) -> str:
def _handleClassPath(
classpath: typing.Union[typing.Sequence[_PathOrStr], _PathOrStr, None] = None,
) -> typing.Sequence[str]:
"""
Return a classpath which represents the given tuple of classpath specifications
"""
out = []

if classpath is None:
return out
if isinstance(classpath, (str, os.PathLike)):
classpath = (classpath,)
try:
Expand All @@ -145,7 +151,7 @@ def _handleClassPath(classpath) -> str:
out.extend(glob.glob(pth + '.jar'))
else:
out.append(pth)
return _classpath._SEP.join(out)
return out


_JVM_started = False
Expand Down Expand Up @@ -221,8 +227,6 @@ def startJVM(
# Allow the path to be a PathLike.
jvmpath = os.fspath(jvmpath)

extra_jvm_args: typing.Tuple[str, ...] = tuple()

# Classpath handling
if _hasClassPath(jvmargs):
# Old style, specified in the arguments
Expand All @@ -233,18 +237,14 @@ def startJVM(
# Not specified at all, use the default classpath.
classpath = _classpath.getClassPath()

# Handle strings and list of strings.
if classpath:
extra_jvm_args += (f'-Djava.class.path={_handleClassPath(classpath)}', )

try:
import locale
# Gather a list of locale settings that Java may override (excluding LC_ALL)
categories = [getattr(locale, i) for i in dir(locale) if i.startswith('LC_') and i != 'LC_ALL']
# Keep the current locale settings, else Java will replace them.
prior = [locale.getlocale(i) for i in categories]
# Start the JVM
_jpype.startup(jvmpath, jvmargs + extra_jvm_args,
_jpype.startup(jvmpath, jvmargs,
ignoreUnrecognized, convertStrings, interrupt)
# Collect required resources for operation
initializeResources()
Expand All @@ -264,6 +264,21 @@ def startJVM(
raise RuntimeError(f"{jvmpath} is older than required Java version{version}") from ex
raise

"""Prior versions of JPype used the jvmargs to setup the class paths via
JNI (Java Native Interface) option strings:
i.e -Djava.class.path=...
See: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/invocation.html
Unfortunately, unicode is unsupported by this interface on windows, since
windows uses wide-byte (16bit) character encoding.
See: https://stackoverflow.com/questions/20052455/jni-start-jvm-with-unicode-support
To resolve this issue we add the classpath after initialization since jpype
itself supports unicode class paths.
"""
for cp in _handleClassPath(classpath):
addClassPath(Path.cwd() / Path(cp).resolve())


def initializeResources():
global _JVM_started
Expand Down Expand Up @@ -348,7 +363,7 @@ def initializeResources():
_jpype.JPypeContext = _jpype.JClass('org.jpype.JPypeContext').getInstance()
_jpype.JPypeClassLoader = _jpype.JPypeContext.getClassLoader()

# Everything successed so started is now true.
# Everything succeeded so started is now true.
_JVM_started = True


Expand Down
29 changes: 25 additions & 4 deletions native/java/org/jpype/pkg/JPypePackageManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
Expand Down Expand Up @@ -458,13 +459,33 @@ private static void collectContents(Map<String, URI> out, Path path2)
}
}

// Java 8 windows bug https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8131067
private static URI toURI(Path path)
{
URI uri = path.toUri();
if (uri.getScheme().equals("jar") && uri.toString().contains("%2520"))
uri = URI.create("jar:" + uri.getRawSchemeSpecificPart().replaceAll("%25", "%"));
return uri;

try {
// Java 8 bug https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8131067
// Zip file system provider returns doubly % encoded URIs. We resolve this
// by re-encoding the URI after decoding it.
uri = new URI(
uri.getScheme(),
URLDecoder.decode(uri.getSchemeSpecificPart()),
uri.getFragment()
);

// `toASCIIString` ensures the URI is URL encoded with only ascii
// characters. This avoids issues in `sun.nio.fs.UnixUriUtils.fromUri` that
// naively uses `uri.getRawPath()` despite the possibility that it contains
// non-ascii characters that will cause errors. By using `toASCIIString` and
// re-wrapping it in a URI object we ensure that the URI is properly
// encoded. See: https://github.com/jpype-project/jpype/issues/1194
return new URI(uri.toASCIIString());
} catch (Exception e) {
// This exception *should* never occur as we are re-encoding a valid URI.
// Throwing a runtime exception avoids java exception handling boilerplate
// for a situation that *should* never occur.
throw new RuntimeException("Failed to encode URI: " + uri, e);
}
}
//</editor-fold>
}
2 changes: 2 additions & 0 deletions project/jars/unicode_à😎/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Test to verify that JAR's with unicode characters in the path are loaded correctly.
The built jar should be placed in ``test/jar/unicode_à😎/sample_package.jar``.
2 changes: 2 additions & 0 deletions project/jars/unicode_à😎/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
javac --release 8 -d classes src/org/jpype/sample_package/*.java
jar --create --file sample_package.jar -C classes .
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jpype.sample_package;

public class A
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jpype.sample_package;

public class B
{
}
Binary file added test/jar/unicode_à😎/sample_package.jar
Binary file not shown.
8 changes: 7 additions & 1 deletion test/jpypetest/test_startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
# *****************************************************************************
import jpype
import subrun
import functools
import os
from pathlib import Path
import unittest
Expand Down Expand Up @@ -146,3 +145,10 @@ def testPathTwice(self):
def testBadKeyword(self):
with self.assertRaises(TypeError):
jpype.startJVM(invalid=True) # type: ignore

def testNonASCIIPath(self):
"""Test that paths with non-ASCII characters are handled correctly.
Regression test for https://github.com/jpype-project/jpype/issues/1194
"""
jpype.startJVM(jvmpath=Path(self.jvmpath), classpath="test/jar/unicode_à😎/sample_package.jar")
assert dir(jpype.JPackage('org.jpype.sample_package')) == ['A', 'B']

0 comments on commit 354968c

Please sign in to comment.