├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── design_discussion.md
│ └── feature_request.md
└── workflows
│ └── check.yml
├── .gitignore
├── LICENSE
├── PLCT.svg
├── README.md
├── base
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── org
│ └── glavo
│ └── japp
│ ├── CompressionMethod.java
│ ├── JAppProperties.java
│ ├── TODO.java
│ ├── annotation
│ └── Visibility.java
│ ├── classfile
│ └── ClassFile.java
│ ├── io
│ ├── ByteBufferChannel.java
│ ├── ByteBufferInputStream.java
│ ├── ByteBufferOutputStream.java
│ ├── IOUtils.java
│ ├── LittleEndianDataOutput.java
│ └── WritableByteChannelWrapper.java
│ └── util
│ ├── ByteBufferUtils.java
│ ├── CompressedNumber.java
│ ├── MUTF8.java
│ ├── MemoryAccess.java
│ ├── XxHash64.java
│ └── ZstdUtils.java
├── bin
├── japp.ps1
└── japp.sh
├── boot
├── build.gradle.kts
└── src
│ └── main
│ ├── java
│ └── org
│ │ └── glavo
│ │ └── japp
│ │ └── boot
│ │ ├── JAppBootArgs.java
│ │ ├── JAppBootLauncher.java
│ │ ├── JAppBootMetadata.java
│ │ ├── JAppReader.java
│ │ ├── JAppResource.java
│ │ ├── JAppResourceField.java
│ │ ├── JAppResourceGroup.java
│ │ ├── JAppResourceRoot.java
│ │ ├── decompressor
│ │ ├── DecompressContext.java
│ │ ├── classfile
│ │ │ ├── ByteArrayPool.java
│ │ │ └── ClassFileDecompressor.java
│ │ └── zstd
│ │ │ ├── BitInputStream.java
│ │ │ ├── Constants.java
│ │ │ ├── FiniteStateEntropy.java
│ │ │ ├── FrameHeader.java
│ │ │ ├── FseCompressionTable.java
│ │ │ ├── FseTableReader.java
│ │ │ ├── Huffman.java
│ │ │ ├── MalformedInputException.java
│ │ │ ├── Util.java
│ │ │ ├── ZstdFrameDecompressor.java
│ │ │ └── package-info.java
│ │ ├── jappfs
│ │ ├── JAppDirectoryStream.java
│ │ ├── JAppFileAttributeView.java
│ │ ├── JAppFileAttributes.java
│ │ ├── JAppFileStore.java
│ │ ├── JAppFileSystem.java
│ │ ├── JAppFileSystemProvider.java
│ │ └── JAppPath.java
│ │ ├── module
│ │ ├── JAppModuleFinder.java
│ │ └── JAppModuleReference.java
│ │ └── url
│ │ ├── JAppURLConnection.java
│ │ ├── JAppURLHandler.java
│ │ ├── JAppURLStreamHandlerFactory.java
│ │ └── JAppURLStreamHandlerProvider.java
│ └── module-info
│ └── module-info.java
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── LWJGL.java
├── docs
├── COMPARE.md
└── introduce.md
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── native
├── .gitignore
├── Cargo.lock
├── Cargo.toml
└── src
│ ├── launcher.rs
│ └── main.rs
├── settings.gradle.kts
├── specification.md
├── src
├── main
│ ├── java
│ │ └── org
│ │ │ └── glavo
│ │ │ └── japp
│ │ │ ├── Main.java
│ │ │ ├── condition
│ │ │ ├── AndCondition.java
│ │ │ ├── Condition.java
│ │ │ ├── ConditionParser.java
│ │ │ ├── JavaCondition.java
│ │ │ ├── MatchList.java
│ │ │ ├── NotCondition.java
│ │ │ └── OrCondition.java
│ │ │ ├── launcher
│ │ │ ├── EmbeddedLauncher.java
│ │ │ ├── JAppConfigGroup.java
│ │ │ ├── JAppLauncherMetadata.java
│ │ │ ├── JAppResourceGroupReference.java
│ │ │ └── Launcher.java
│ │ │ ├── maven
│ │ │ ├── MavenRepository.java
│ │ │ └── MavenResolver.java
│ │ │ ├── packer
│ │ │ ├── JAppPacker.java
│ │ │ ├── JAppResourceInfo.java
│ │ │ ├── JAppResourcesWriter.java
│ │ │ ├── JAppWriter.java
│ │ │ ├── ModuleInfoReader.java
│ │ │ ├── compressor
│ │ │ │ ├── CompressContext.java
│ │ │ │ ├── CompressResult.java
│ │ │ │ ├── Compressor.java
│ │ │ │ ├── Compressors.java
│ │ │ │ ├── DefaultCompressor.java
│ │ │ │ └── classfile
│ │ │ │ │ ├── ByteArrayPoolBuilder.java
│ │ │ │ │ ├── ClassFileCompressor.java
│ │ │ │ │ └── ClassFileReader.java
│ │ │ └── processor
│ │ │ │ ├── ClassPathProcessor.java
│ │ │ │ ├── LocalClassPathProcessor.java
│ │ │ │ ├── MavenClassPathProcessor.java
│ │ │ │ └── PathListParser.java
│ │ │ └── platform
│ │ │ ├── Architecture.java
│ │ │ ├── JAppRuntimeContext.java
│ │ │ ├── JavaRuntime.java
│ │ │ ├── LibC.java
│ │ │ └── OperatingSystem.java
│ └── resources
│ │ └── org
│ │ └── glavo
│ │ └── japp
│ │ └── packer
│ │ └── header.sh
└── test
│ └── java
│ └── org
│ └── glavo
│ └── japp
│ ├── boot
│ ├── JAppResourceTest.java
│ └── decompressor
│ │ └── zstd
│ │ └── ZstdTest.java
│ ├── classfile
│ └── ByteArrayPoolTest.java
│ ├── launcher
│ └── EndZipTest.java
│ ├── packer
│ ├── ModuleInfoReaderTest.java
│ ├── compressor
│ │ └── ClassFileCompressorTest.java
│ └── processor
│ │ └── PathListParserTest.java
│ ├── testcase
│ ├── HelloWorldTest.java
│ ├── JAppTestHelper.java
│ ├── JAppTestTemplate.java
│ └── ModulePathTest.java
│ └── util
│ ├── CompressedNumberTest.java
│ ├── MUTF8Test.java
│ └── XxHash64Test.java
└── test-case
├── HelloWorld
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ ├── module-info.java
│ └── org
│ └── glavo
│ └── japp
│ └── testcase
│ └── helloworld
│ └── HelloWorld.java
└── ModulePath
├── build.gradle.kts
└── src
└── main
└── java
└── org
└── glavo
└── japp
└── testcase
└── modulepath
└── ModulePath.java
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://donate.glavo.site/"]
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: '[Bug] '
5 | labels: ['bug']
6 | assignees: ''
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/design_discussion.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Design Discussion
3 | about: Discuss japp file and launcher design
4 | title: '[Design] '
5 | labels: ['design']
6 | assignees: ''
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: '[Feature] '
5 | labels: ['enhancement']
6 | assignees: ''
7 |
8 | ---
9 |
10 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Gradle Check
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | gradle-check:
11 | runs-on: ubuntu-22.04
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-java@v3
15 | with:
16 | distribution: 'temurin'
17 | java-version: '21'
18 | - uses: gradle/gradle-build-action@v2
19 | with:
20 | arguments: check --info --no-daemon --stacktrace
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.japp
2 | /.japp
3 | .gradle
4 | build/
5 | !gradle/wrapper/gradle-wrapper.jar
6 | !**/src/main/**/build/
7 | !**/src/test/**/build/
8 |
9 | ### IntelliJ IDEA ###
10 | .idea
11 |
12 | ### Eclipse ###
13 | .apt_generated
14 | .classpath
15 | .factorypath
16 | .project
17 | .settings
18 | .springBeans
19 | .sts4-cache
20 | !**/src/main/**/bin/
21 | !**/src/test/**/bin/
22 |
23 | ### NetBeans ###
24 | /nbproject/private/
25 | /nbbuild/
26 | /dist/
27 | /nbdist/
28 | /.nb-gradle/
29 |
30 | ### VS Code ###
31 | .vscode/
32 |
33 | ### Mac OS ###
34 | .DS_Store
35 |
36 | # Other
37 |
38 |
--------------------------------------------------------------------------------
/PLCT.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/base/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | tasks.compileJava {
4 | options.compilerArgs.addAll(
5 | listOf(
6 | "--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED",
7 | "--add-exports=java.base/jdk.internal.module=ALL-UNNAMED",
8 | "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED",
9 | )
10 | )
11 | }
12 |
13 | val jappPropertiesFile = rootProject.layout.buildDirectory.file("japp.properties").get().asFile
14 |
15 | tasks.create("generateJAppProperties") {
16 | inputs
17 | outputs.file(jappPropertiesFile)
18 | doLast {
19 | val properties = Properties()
20 | properties["Project-Directory"] = rootProject.layout.projectDirectory.asFile.absolutePath
21 | properties["Boot-Jar"] = project(":boot").tasks.getByName("bootJar").archiveFile.get().asFile.absolutePath
22 | jappPropertiesFile.writer().use { writer ->
23 | properties.store(writer, null)
24 | }
25 | }
26 | }
27 |
28 | tasks.processResources {
29 | dependsOn("generateJAppProperties")
30 |
31 | into("org/glavo/japp") {
32 | from(jappPropertiesFile)
33 | }
34 | }
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/CompressionMethod.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp;
17 |
18 | import java.io.IOException;
19 | import java.nio.ByteBuffer;
20 |
21 | public enum CompressionMethod {
22 | NONE,
23 | CLASSFILE,
24 | ZSTD;
25 |
26 | private static final CompressionMethod[] METHODS = values();
27 |
28 | public static CompressionMethod of(int i) {
29 | return i >= 0 && i < METHODS.length ? METHODS[i] : null;
30 | }
31 |
32 | public static CompressionMethod readFrom(ByteBuffer buffer) throws IOException {
33 | byte id = buffer.get();
34 | if (id >= 0 && id < METHODS.length) {
35 | return METHODS[id];
36 | }
37 |
38 | throw new IOException(String.format("Unknown compression method: 0x%02x", Byte.toUnsignedInt(id)));
39 | }
40 |
41 | public byte id() {
42 | return (byte) ordinal();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/JAppProperties.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp;
17 |
18 | import java.io.InputStreamReader;
19 | import java.io.Reader;
20 | import java.nio.file.Path;
21 | import java.nio.file.Paths;
22 | import java.util.Properties;
23 |
24 | import static java.nio.charset.StandardCharsets.UTF_8;
25 |
26 | public final class JAppProperties {
27 |
28 | private static final Path PROJECT_DIRECTORY;
29 | private static final Path HOME_DIRECTORY;
30 | private static final Path BOOT_JAR;
31 |
32 | static {
33 | Properties properties = new Properties();
34 |
35 | //noinspection DataFlowIssue
36 | try (Reader reader = new InputStreamReader(JAppProperties.class.getResourceAsStream("japp.properties"), UTF_8)) {
37 | properties.load(reader);
38 | } catch (Exception e) {
39 | throw new AssertionError(e);
40 | }
41 |
42 | // In the early stages we isolate the configuration in the project directory
43 | PROJECT_DIRECTORY = Paths.get(properties.getProperty("Project-Directory"));
44 | HOME_DIRECTORY = PROJECT_DIRECTORY.resolve(".japp");
45 | BOOT_JAR = Paths.get(properties.getProperty("Boot-Jar"));
46 | }
47 |
48 | public static Path getProjectDirectory() {
49 | return PROJECT_DIRECTORY;
50 | }
51 |
52 | public static Path getHomeDirectory() {
53 | return HOME_DIRECTORY;
54 | }
55 |
56 | public static Path getBootJar() {
57 | return BOOT_JAR;
58 | }
59 |
60 | private JAppProperties() {
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/TODO.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp;
17 |
18 | public final class TODO extends Error {
19 | public TODO() {
20 | super("TODO");
21 | }
22 |
23 | public TODO(String message) {
24 | super("TODO: " + message);
25 | }
26 |
27 | public TODO(String message, Throwable cause) {
28 | super("TODO: " + message, cause);
29 | }
30 |
31 | public TODO(Throwable cause) {
32 | super("TODO", cause);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/annotation/Visibility.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.annotation;
17 |
18 | import java.lang.annotation.Retention;
19 | import java.lang.annotation.RetentionPolicy;
20 |
21 | @Retention(RetentionPolicy.SOURCE)
22 | public @interface Visibility {
23 | enum Context {
24 | BOOT,
25 | LAUNCHER,
26 | PACKER
27 | }
28 |
29 | Context[] value();
30 | }
31 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/classfile/ClassFile.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.classfile;
17 |
18 | public final class ClassFile {
19 |
20 | public static final int MAGIC_NUMBER = 0xcafebabe;
21 |
22 | public static final byte CONSTANT_Utf8 = 1;
23 | public static final byte CONSTANT_Integer = 3;
24 | public static final byte CONSTANT_Float = 4;
25 | public static final byte CONSTANT_Long = 5;
26 | public static final byte CONSTANT_Double = 6;
27 | public static final byte CONSTANT_Class = 7;
28 | public static final byte CONSTANT_String = 8;
29 | public static final byte CONSTANT_Fieldref = 9;
30 | public static final byte CONSTANT_Methodref = 10;
31 | public static final byte CONSTANT_InterfaceMethodref = 11;
32 | public static final byte CONSTANT_NameAndType = 12;
33 | public static final byte CONSTANT_MethodHandle = 15;
34 | public static final byte CONSTANT_MethodType = 16;
35 | public static final byte CONSTANT_Dynamic = 17;
36 | public static final byte CONSTANT_InvokeDynamic = 18;
37 | public static final byte CONSTANT_Module = 19;
38 | public static final byte CONSTANT_Package = 20;
39 |
40 | public static final byte CONSTANT_EXTERNAL_STRING = -1;
41 | public static final byte CONSTANT_EXTERNAL_STRING_Class = -2;
42 | public static final byte CONSTANT_EXTERNAL_STRING_Descriptor = -3;
43 | public static final byte CONSTANT_EXTERNAL_STRING_Signature = -4;
44 |
45 | public static final byte[] CONSTANT_SIZE = new byte[32];
46 | static {
47 | CONSTANT_SIZE[CONSTANT_Integer] = 4;
48 | CONSTANT_SIZE[CONSTANT_Float] = 4;
49 | CONSTANT_SIZE[CONSTANT_Long] = 8;
50 | CONSTANT_SIZE[CONSTANT_Double] = 8;
51 | CONSTANT_SIZE[CONSTANT_Class] = 2;
52 | CONSTANT_SIZE[CONSTANT_String] = 2;
53 | CONSTANT_SIZE[CONSTANT_Fieldref] = 4;
54 | CONSTANT_SIZE[CONSTANT_Methodref] = 4;
55 | CONSTANT_SIZE[CONSTANT_InterfaceMethodref] = 4;
56 | CONSTANT_SIZE[CONSTANT_NameAndType] = 4;
57 | CONSTANT_SIZE[CONSTANT_MethodHandle] = 3;
58 | CONSTANT_SIZE[CONSTANT_MethodType] = 2;
59 | CONSTANT_SIZE[CONSTANT_Dynamic] = 4;
60 | CONSTANT_SIZE[CONSTANT_InvokeDynamic] = 4;
61 | CONSTANT_SIZE[CONSTANT_Module] = 2;
62 | CONSTANT_SIZE[CONSTANT_Package] = 2;
63 | }
64 |
65 | private ClassFile() {
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/io/ByteBufferChannel.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.io;
17 |
18 | import java.io.IOException;
19 | import java.nio.ByteBuffer;
20 | import java.nio.channels.ClosedChannelException;
21 | import java.nio.channels.NonWritableChannelException;
22 | import java.nio.channels.SeekableByteChannel;
23 |
24 | public final class ByteBufferChannel implements SeekableByteChannel {
25 | private ByteBuffer buffer;
26 |
27 | public ByteBufferChannel(ByteBuffer buffer) {
28 | this.buffer = buffer;
29 | }
30 |
31 | private void ensureOpen() throws IOException {
32 | if (buffer == null) {
33 | throw new ClosedChannelException();
34 | }
35 | }
36 |
37 | @Override
38 | public boolean isOpen() {
39 | return buffer != null;
40 | }
41 |
42 | @Override
43 | public long position() throws IOException {
44 | ensureOpen();
45 | return buffer.position();
46 | }
47 |
48 | @Override
49 | public long size() throws IOException {
50 | ensureOpen();
51 | return buffer.capacity();
52 | }
53 |
54 | @Override
55 | public int read(ByteBuffer dst) throws IOException {
56 | ensureOpen();
57 |
58 | int remaining = buffer.remaining();
59 |
60 | if (remaining == 0) {
61 | return -1;
62 | }
63 |
64 | int n = Math.min(dst.remaining(), remaining);
65 | int end = buffer.position() + n;
66 | dst.put(buffer.duplicate().limit(end));
67 | buffer.position(end);
68 | return n;
69 | }
70 |
71 | @Override
72 | public void close() throws IOException {
73 | buffer = null;
74 | }
75 |
76 | @Override
77 | public int write(ByteBuffer src) throws IOException {
78 | throw new NonWritableChannelException();
79 | }
80 |
81 | @Override
82 | public SeekableByteChannel position(long newPosition) throws IOException {
83 | if (newPosition < 0 || newPosition >= Integer.MAX_VALUE) {
84 | throw new IllegalArgumentException("Illegal position " + newPosition);
85 | }
86 | this.buffer.position(Math.min((int) newPosition, buffer.limit()));
87 | return this;
88 | }
89 |
90 | @Override
91 | public SeekableByteChannel truncate(long size) throws IOException {
92 | throw new NonWritableChannelException();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/io/ByteBufferInputStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.io;
17 |
18 | import java.io.EOFException;
19 | import java.io.IOException;
20 | import java.io.InputStream;
21 | import java.nio.ByteBuffer;
22 | import java.nio.InvalidMarkException;
23 | import java.util.Objects;
24 |
25 | public final class ByteBufferInputStream extends InputStream {
26 |
27 | private final ByteBuffer buffer;
28 |
29 | public ByteBufferInputStream(byte[] array) {
30 | this.buffer = ByteBuffer.wrap(array);
31 | }
32 |
33 | public ByteBufferInputStream(ByteBuffer buffer) {
34 | this.buffer = buffer;
35 | }
36 |
37 | @Override
38 | public int available() throws IOException {
39 | return buffer.remaining();
40 | }
41 |
42 | @Override
43 | public int read() throws IOException {
44 | if (buffer.hasRemaining()) {
45 | return Byte.toUnsignedInt(buffer.get());
46 | } else {
47 | return -1;
48 | }
49 | }
50 |
51 | @Override
52 | public int read(byte[] b, int off, int len) throws IOException {
53 | Objects.checkFromIndexSize(off, len, b.length);
54 | if (len == 0) {
55 | return 0;
56 | }
57 |
58 | int remaining = buffer.remaining();
59 | if (remaining == 0) {
60 | return -1;
61 | }
62 |
63 | int n = Math.min(remaining, len);
64 | buffer.get(b, off, n);
65 | return n;
66 | }
67 |
68 | // @Override
69 | public byte[] readAllBytes() throws IOException {
70 | int remaining = buffer.remaining();
71 | byte[] res = new byte[remaining];
72 | buffer.get(res);
73 | return res;
74 | }
75 |
76 | @Override
77 | public long skip(long n) throws IOException {
78 | if (n <= 0) {
79 | return 0;
80 | }
81 |
82 | int res = (int) Math.min(n, buffer.remaining());
83 | buffer.position(buffer.position() + res);
84 | return res;
85 | }
86 |
87 | // @Override
88 | @SuppressWarnings("Since15")
89 | public void skipNBytes(long n) throws IOException {
90 | if (n <= 0) {
91 | return;
92 | }
93 |
94 | int remaining = buffer.remaining();
95 | if (n > remaining) {
96 | throw new EOFException();
97 | }
98 |
99 | int res = (int) Math.min(n, buffer.remaining());
100 | buffer.position(buffer.position() + res);
101 | }
102 |
103 | @Override
104 | public boolean markSupported() {
105 | return true;
106 | }
107 |
108 | @Override
109 | public void mark(int readlimit) {
110 | buffer.mark();
111 | }
112 |
113 | @Override
114 | public void reset() throws IOException {
115 | try {
116 | buffer.reset();
117 | } catch (InvalidMarkException e) {
118 | throw new IOException(e);
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/io/ByteBufferOutputStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.io;
17 |
18 | import java.io.IOException;
19 | import java.io.OutputStream;
20 | import java.nio.ByteBuffer;
21 | import java.nio.ByteOrder;
22 | import java.nio.charset.StandardCharsets;
23 | import java.util.Arrays;
24 |
25 | public final class ByteBufferOutputStream extends LittleEndianDataOutput {
26 | private ByteBuffer buffer;
27 |
28 | public ByteBufferOutputStream() {
29 | this(ByteOrder.LITTLE_ENDIAN, 8192);
30 | }
31 |
32 | public ByteBufferOutputStream(int initialCapacity) {
33 | this(ByteOrder.LITTLE_ENDIAN, initialCapacity);
34 | }
35 |
36 | public ByteBufferOutputStream(ByteOrder order, int initialCapacity) {
37 | this.buffer = ByteBuffer.allocate(initialCapacity).order(order);
38 | }
39 |
40 | public ByteBufferOutputStream(ByteBuffer buffer) {
41 | this.buffer = buffer;
42 | }
43 |
44 | private void prepare(int next) {
45 | if (buffer.remaining() < next) {
46 | byte[] arr = buffer.array();
47 | int prevLen = buffer.position();
48 | int nextLen = Math.max(prevLen * 2, prevLen + next);
49 |
50 | buffer = ByteBuffer.allocate(nextLen).order(buffer.order());
51 | buffer.put(arr, 0, prevLen);
52 | }
53 | }
54 |
55 | public ByteBuffer getByteBuffer() {
56 | return buffer;
57 | }
58 |
59 | public long getTotalBytes() {
60 | return buffer.position();
61 | }
62 |
63 | @Override
64 | public void write(int b) {
65 | writeByte((byte) b);
66 | }
67 |
68 | @Override
69 | public void write(byte[] b, int off, int len) {
70 | writeBytes(b, off, len);
71 | }
72 |
73 | public void writeByte(byte v) {
74 | prepare(Byte.BYTES);
75 | buffer.put(v);
76 | }
77 |
78 | public void writeShort(short v) {
79 | prepare(Short.BYTES);
80 | buffer.putShort(v);
81 | }
82 |
83 | public void writeInt(int v) {
84 | prepare(Integer.BYTES);
85 | buffer.putInt(v);
86 | }
87 |
88 | public void writeLong(long v) {
89 | prepare(Long.BYTES);
90 | buffer.putLong(v);
91 | }
92 |
93 | public void writeBytes(byte[] array, int offset, int len) {
94 | prepare(len);
95 | buffer.put(array, offset, len);
96 | }
97 |
98 | public void writeTo(OutputStream out) throws IOException {
99 | out.write(buffer.array(), 0, buffer.position());
100 | }
101 |
102 | public byte[] toByteArray() {
103 | return Arrays.copyOf(buffer.array(), buffer.position());
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/io/IOUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.io;
17 |
18 | import java.io.EOFException;
19 | import java.io.IOException;
20 | import java.nio.ByteBuffer;
21 | import java.nio.channels.ReadableByteChannel;
22 | import java.nio.channels.WritableByteChannel;
23 |
24 | public final class IOUtils {
25 |
26 | public static void readFully(ReadableByteChannel channel, ByteBuffer buffer) throws IOException {
27 |
28 | //noinspection StatementWithEmptyBody
29 | while (channel.read(buffer) > 0) {
30 | }
31 |
32 | if (buffer.hasRemaining()) {
33 | throw new EOFException("Unexpected end of data");
34 | }
35 | }
36 |
37 | public static void writeFully(WritableByteChannel channel, ByteBuffer buffer) throws IOException {
38 | while (buffer.hasRemaining()) {
39 | channel.write(buffer);
40 | }
41 | }
42 |
43 | private IOUtils() {
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/io/LittleEndianDataOutput.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.io;
17 |
18 | import java.io.Closeable;
19 | import java.io.IOException;
20 | import java.io.OutputStream;
21 | import java.nio.channels.Channels;
22 | import java.nio.channels.WritableByteChannel;
23 | import java.nio.charset.StandardCharsets;
24 |
25 | public abstract class LittleEndianDataOutput extends OutputStream implements Closeable {
26 |
27 | public static LittleEndianDataOutput of(OutputStream outputStream) {
28 | return new WritableByteChannelWrapper(Channels.newChannel(outputStream));
29 | }
30 |
31 | public static LittleEndianDataOutput of(WritableByteChannel channel) {
32 | return new WritableByteChannelWrapper(channel);
33 | }
34 |
35 | public abstract long getTotalBytes();
36 |
37 | public abstract void writeByte(byte v) throws IOException;
38 |
39 | public void writeUnsignedByte(int v) throws IOException {
40 | if (v < 0 || v > 0xff) {
41 | throw new IllegalArgumentException();
42 | }
43 |
44 | writeByte((byte) v);
45 | }
46 |
47 | public abstract void writeShort(short v) throws IOException;
48 |
49 | public void writeUnsignedShort(int v) throws IOException {
50 | if (v < 0 || v > 0xffff) {
51 | throw new IllegalArgumentException();
52 | }
53 |
54 | writeShort((short) v);
55 | }
56 |
57 | public abstract void writeInt(int v) throws IOException;
58 |
59 | public void writeUnsignedInt(long v) throws IOException {
60 | if (v < 0 || v > 0xffff_ffffL) {
61 | throw new IllegalArgumentException();
62 | }
63 |
64 | writeInt((int) v);
65 | }
66 |
67 | public abstract void writeLong(long v) throws IOException;
68 |
69 | public void writeBytes(byte[] array) throws IOException {
70 | writeBytes(array, 0, array.length);
71 | }
72 |
73 | public abstract void writeBytes(byte[] array, int offset, int len) throws IOException;
74 |
75 | public void writeString(String str) throws IOException {
76 | if (str != null) {
77 | byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
78 | writeInt(bytes.length);
79 | writeBytes(bytes);
80 | } else {
81 | writeInt(0);
82 | }
83 | }
84 |
85 | @Override
86 | public void write(int b) throws IOException {
87 | LittleEndianDataOutput.this.writeByte((byte) b);
88 | }
89 |
90 | @Override
91 | public void write(byte[] b, int off, int len) throws IOException {
92 | LittleEndianDataOutput.this.writeBytes(b, off, len);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/io/WritableByteChannelWrapper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.io;
17 |
18 | import java.io.IOException;
19 | import java.io.OutputStream;
20 | import java.nio.ByteBuffer;
21 | import java.nio.ByteOrder;
22 | import java.nio.channels.Channels;
23 | import java.nio.channels.WritableByteChannel;
24 | import java.util.Objects;
25 |
26 | final class WritableByteChannelWrapper extends LittleEndianDataOutput {
27 | private final WritableByteChannel channel;
28 | private final ByteBuffer buffer = ByteBuffer.allocate(8192).order(ByteOrder.LITTLE_ENDIAN);
29 | private long totalBytes = 0L;
30 |
31 | WritableByteChannelWrapper(OutputStream outputStream) {
32 | this.channel = Channels.newChannel(outputStream);
33 | }
34 |
35 | WritableByteChannelWrapper(WritableByteChannel channel) {
36 | this.channel = channel;
37 | }
38 |
39 | private void flushBuffer() throws IOException {
40 | if (buffer.position() > 0) {
41 | buffer.flip();
42 | IOUtils.writeFully(channel, buffer);
43 | buffer.clear();
44 | }
45 | }
46 |
47 | private void prepare(int next) throws IOException {
48 | if (buffer.remaining() < next) {
49 | flushBuffer();
50 | }
51 | }
52 |
53 | @Override
54 | public long getTotalBytes() {
55 | return totalBytes;
56 | }
57 |
58 | @Override
59 | public void writeByte(byte v) throws IOException {
60 | prepare(Byte.BYTES);
61 | buffer.put(v);
62 | totalBytes += Byte.BYTES;
63 | }
64 |
65 | @Override
66 | public void writeShort(short v) throws IOException {
67 | prepare(Short.BYTES);
68 | buffer.putShort(v);
69 | totalBytes += Short.BYTES;
70 | }
71 |
72 | @Override
73 | public void writeInt(int v) throws IOException {
74 | prepare(Integer.BYTES);
75 | buffer.putInt(v);
76 | totalBytes += Integer.BYTES;
77 | }
78 |
79 | @Override
80 | public void writeLong(long v) throws IOException {
81 | prepare(Long.BYTES);
82 | buffer.putLong(v);
83 | totalBytes += Long.BYTES;
84 | }
85 |
86 | @Override
87 | public void writeBytes(byte[] array, int offset, int len) throws IOException {
88 | Objects.checkFromIndexSize(offset, len, array.length);
89 | if (len == 0) {
90 | return;
91 | }
92 |
93 | if (len < buffer.capacity()) {
94 | if (len > buffer.remaining()) {
95 | flushBuffer();
96 | }
97 | buffer.put(array, offset, len);
98 | } else {
99 | flushBuffer();
100 | IOUtils.writeFully(channel, ByteBuffer.wrap(array, offset, len));
101 | }
102 | totalBytes += len;
103 | }
104 |
105 | @Override
106 | public void close() throws IOException {
107 | try {
108 | flushBuffer();
109 | } finally {
110 | channel.close();
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/util/ByteBufferUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import java.nio.ByteBuffer;
19 | import java.nio.charset.StandardCharsets;
20 | import java.util.List;
21 |
22 | public final class ByteBufferUtils {
23 |
24 | @SuppressWarnings("deprecation")
25 | private static final int release = Runtime.version().major();
26 |
27 | public static ByteBuffer slice(ByteBuffer buffer, int index, int length) {
28 | if (release >= 13) {
29 | //noinspection Since15
30 | return buffer.slice(index, length);
31 | } else {
32 | return buffer.duplicate().limit(index + length).position(index).slice();
33 | }
34 | }
35 |
36 | public static String readString(ByteBuffer buffer) {
37 | int length = buffer.getInt();
38 | byte[] array = new byte[length];
39 | buffer.get(array);
40 | return new String(array, StandardCharsets.UTF_8);
41 | }
42 |
43 | public static String readStringOrNull(ByteBuffer buffer) {
44 | int length = buffer.getInt();
45 | if (length == 0) {
46 | return null;
47 | }
48 |
49 | byte[] array = new byte[length];
50 | buffer.get(array);
51 | return new String(array, StandardCharsets.UTF_8);
52 | }
53 |
54 | public static void readStringList(ByteBuffer buffer, List super String> out) {
55 | int count = buffer.getInt();
56 | for (int i = 0; i < count; i++) {
57 | out.add(readString(buffer));
58 | }
59 | }
60 |
61 | private ByteBufferUtils() {
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/util/CompressedNumber.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import java.nio.ByteBuffer;
19 |
20 | public final class CompressedNumber {
21 |
22 | private static final int SIGN_MASK = 0b1000_0000;
23 | private static final int VALUE_MASK = 0b0111_1111;
24 | private static final int TAIL_VALUE_MASK = 0b0111;
25 |
26 | public static void putInt(ByteBuffer out, int value) {
27 | if (value < 0) {
28 | throw new AssertionError();
29 | }
30 | do {
31 | int b = value & VALUE_MASK;
32 | value >>>= 7;
33 |
34 | if (value != 0) {
35 | b |= SIGN_MASK;
36 | }
37 |
38 | out.put((byte) b);
39 | } while (value != 0);
40 | }
41 |
42 | public static int getInt(ByteBuffer in) {
43 | int res = 0;
44 | for (int i = 0; i < 4; i++) {
45 | int b = Byte.toUnsignedInt(in.get());
46 | int bv = b & VALUE_MASK;
47 |
48 | res = res | (bv << (7 * i));
49 | if (b == bv) {
50 | return res;
51 | }
52 | }
53 |
54 | int b = Byte.toUnsignedInt(in.get());
55 | int bv = b & TAIL_VALUE_MASK;
56 | if (b != bv) {
57 | throw new AssertionError();
58 | }
59 |
60 | return res | (bv << (7 * 4));
61 | }
62 |
63 | private CompressedNumber() {
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/util/MUTF8.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import java.nio.charset.StandardCharsets;
19 |
20 | public final class MUTF8 {
21 | public static String stringFromMUTF8(byte[] bytes, int offset, int count) {
22 | final int end = offset + count;
23 |
24 | int i;
25 | for (i = offset; i < end; i++) {
26 | if ((bytes[i] & 0x80) != 0) {
27 | break;
28 | }
29 | }
30 |
31 | if (i == end) {
32 | return new String(bytes, offset, count, StandardCharsets.US_ASCII);
33 | }
34 |
35 | StringBuilder builder = new StringBuilder(count);
36 | for (i = offset; i < end; i++) {
37 | byte ch = bytes[i];
38 |
39 | if (ch == 0) {
40 | break;
41 | }
42 |
43 | if (ch > 0) {
44 | builder.append((char) ch);
45 | } else {
46 | int uch = ch & 0x7F;
47 | int mask = 0x40;
48 |
49 | while ((uch & mask) != 0) {
50 | ch = bytes[++i];
51 |
52 | if ((ch & 0xC0) != 0x80) {
53 | throw new IllegalArgumentException("bad continuation 0x" + Integer.toHexString(ch));
54 | }
55 |
56 | uch = ((uch & ~mask) << 6) | (ch & 0x3F);
57 | mask <<= 6 - 1;
58 | }
59 |
60 | if ((uch & 0xFFFF) != uch) {
61 | throw new IllegalArgumentException("character out of range \\u" + Integer.toHexString(uch));
62 | }
63 | builder.appendCodePoint(uch);
64 | }
65 | }
66 | return builder.toString();
67 | }
68 |
69 | public static String stringFromMUTF8(byte[] bytes) {
70 | return stringFromMUTF8(bytes, 0, bytes.length);
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/util/MemoryAccess.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import jdk.internal.misc.Unsafe;
19 |
20 | import java.nio.Buffer;
21 | import java.nio.ByteBuffer;
22 |
23 | public final class MemoryAccess {
24 | private static final Unsafe UNSAFE = Unsafe.getUnsafe();
25 | private static final long BUFFER_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(Buffer.class, "address");
26 |
27 | public static final int ARRAY_BYTE_BASE_OFFSET = Unsafe.ARRAY_BYTE_BASE_OFFSET;
28 |
29 | private MemoryAccess() {
30 | }
31 |
32 | public static long getDirectBufferAddress(ByteBuffer buffer) {
33 | assert buffer.isDirect();
34 | return UNSAFE.getLong(buffer, BUFFER_ADDRESS_OFFSET);
35 | }
36 |
37 | public static byte getByte(Object o, long offset) {
38 | return UNSAFE.getByte(o, offset);
39 | }
40 |
41 | public static int getUnsignedByte(Object o, long offset) {
42 | return Byte.toUnsignedInt(getByte(o, offset));
43 | }
44 |
45 | public static void putByte(Object o, long offset, byte x) {
46 | UNSAFE.putByte(o, offset, x);
47 | }
48 |
49 | public static short getShort(Object o, long offset) {
50 | return UNSAFE.getShortUnaligned(o, offset, false);
51 | }
52 |
53 | public static void putShort(Object o, long offset, short x) {
54 | UNSAFE.putShortUnaligned(o, offset, x, false);
55 | }
56 |
57 | public static int getInt(Object o, long offset) {
58 | return UNSAFE.getIntUnaligned(o, offset, false);
59 | }
60 |
61 | public static long getUnsignedInt(Object o, long offset) {
62 | return Integer.toUnsignedLong(getInt(o, offset));
63 | }
64 |
65 | public static void putInt(Object o, long offset, int x) {
66 | UNSAFE.putIntUnaligned(o, offset, x, false);
67 | }
68 |
69 | public static long getLong(Object o, long offset) {
70 | return UNSAFE.getLongUnaligned(o, offset, false);
71 | }
72 |
73 | public static void putLong(Object o, long offset, long x) {
74 | UNSAFE.putLongUnaligned(o, offset, x, false);
75 | }
76 |
77 | public static void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes) {
78 | UNSAFE.copyMemory(srcBase, srcOffset, destBase, destOffset, bytes);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/util/XxHash64.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import java.lang.ref.Reference;
19 | import java.nio.ByteBuffer;
20 |
21 | import static org.glavo.japp.util.MemoryAccess.ARRAY_BYTE_BASE_OFFSET;
22 |
23 | public final class XxHash64 {
24 | private static final long P1 = 0x9E3779B185EBCA87L;
25 | private static final long P2 = 0xC2B2AE3D27D4EB4FL;
26 | private static final long P3 = 0x165667B19E3779F9L;
27 | private static final long P4 = 0x85EBCA77C2b2AE63L;
28 | private static final long P5 = 0x27D4EB2F165667C5L;
29 |
30 | public static long hashByteBufferWithoutUpdate(ByteBuffer buffer) {
31 | return hashByteBufferWithoutUpdate(0L, buffer);
32 | }
33 |
34 | public static long hashByteBufferWithoutUpdate(long seed, ByteBuffer buffer) {
35 | Object inputBase;
36 | long inputAddress;
37 | long inputLimit;
38 |
39 | if (buffer.hasArray()) {
40 | inputBase = buffer.array();
41 | inputAddress = ARRAY_BYTE_BASE_OFFSET + buffer.arrayOffset() + buffer.position();
42 | } else {
43 | inputBase = null;
44 | inputAddress = MemoryAccess.getDirectBufferAddress(buffer);
45 | }
46 | inputLimit = inputAddress + buffer.remaining();
47 |
48 | try {
49 | return hash(seed, inputBase, inputAddress, inputLimit);
50 | } finally {
51 | Reference.reachabilityFence(buffer);
52 | }
53 | }
54 |
55 | public static long hash(byte[] array) {
56 | return hash(0, array);
57 | }
58 |
59 | public static long hash(long seed, byte[] array) {
60 | return hash(seed, array, 0, array.length);
61 | }
62 |
63 | public static long hash(long seed, byte[] array, int offset, int length) {
64 | return hash(seed, (Object) array, ARRAY_BYTE_BASE_OFFSET + offset, ARRAY_BYTE_BASE_OFFSET + offset + length);
65 | }
66 |
67 | public static long hash(long seed, Object inputBase, long inputAddress, long inputLimit) {
68 | long hash;
69 | long address = inputAddress;
70 |
71 | if (inputLimit - address >= 32) {
72 | long v1 = seed + P1 + P2;
73 | long v2 = seed + P2;
74 | long v3 = seed;
75 | long v4 = seed - P1;
76 |
77 | do {
78 | v1 = mix(v1, MemoryAccess.getLong(inputBase, address));
79 | v2 = mix(v2, MemoryAccess.getLong(inputBase, address + 8));
80 | v3 = mix(v3, MemoryAccess.getLong(inputBase, address + 16));
81 | v4 = mix(v4, MemoryAccess.getLong(inputBase, address + 24));
82 |
83 | address += 32;
84 | } while (inputLimit - address >= 32);
85 |
86 | hash = Long.rotateLeft(v1, 1)
87 | + Long.rotateLeft(v2, 7)
88 | + Long.rotateLeft(v3, 12)
89 | + Long.rotateLeft(v4, 18);
90 |
91 | hash = update(hash, v1);
92 | hash = update(hash, v2);
93 | hash = update(hash, v3);
94 | hash = update(hash, v4);
95 | } else {
96 | hash = seed + P5;
97 | }
98 |
99 | hash += inputLimit - inputAddress;
100 |
101 | while (address <= inputLimit - 8) {
102 | long k1 = MemoryAccess.getLong(inputBase, address);
103 | k1 *= P2;
104 | k1 = Long.rotateLeft(k1, 31);
105 | k1 *= P1;
106 | hash ^= k1;
107 | hash = Long.rotateLeft(hash, 27) * P1 + P4;
108 | address += 8;
109 | }
110 |
111 | if (address <= inputLimit - 4) {
112 | hash ^= MemoryAccess.getUnsignedInt(inputBase, address) * P1;
113 | hash = Long.rotateLeft(hash, 23) * P2 + P3;
114 | address += 4;
115 | }
116 |
117 | while (address < inputLimit) {
118 | hash ^= MemoryAccess.getUnsignedByte(inputBase, address) * P5;
119 | hash = Long.rotateLeft(hash, 11) * P1;
120 | address++;
121 | }
122 |
123 | return finalize(hash);
124 | }
125 |
126 | private static long mix(long current, long value) {
127 | return Long.rotateLeft(current + value * P2, 31) * P1;
128 | }
129 |
130 | private static long update(long hash, long value) {
131 | return (hash ^ mix(0, value)) * P1 + P4;
132 | }
133 |
134 | private static long finalize(long hash) {
135 | hash ^= hash >>> 33;
136 | hash *= P2;
137 | hash ^= hash >>> 29;
138 | hash *= P3;
139 | hash ^= hash >>> 32;
140 | return hash;
141 | }
142 |
143 | }
144 |
145 |
--------------------------------------------------------------------------------
/base/src/main/java/org/glavo/japp/util/ZstdUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | public final class ZstdUtils {
19 | public static int maxCompressedLength(int sourceLength) {
20 | int maxCompressedSize = sourceLength + (sourceLength >>> 8);
21 |
22 | if (sourceLength < 128 * 1024) {
23 | maxCompressedSize += (128 * 1024 - sourceLength) >>> 11;
24 | }
25 | return maxCompressedSize;
26 | }
27 |
28 | private ZstdUtils() {
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/bin/japp.ps1:
--------------------------------------------------------------------------------
1 | $ScriptPath = $MyInvocation.MyCommand.Path
2 | $ProjectDir = Resolve-Path $ScriptPath\..\..
3 | $JAppJar = "$ProjectDir\build\japp.jar"
4 |
5 | if(-Not(Test-Path -Path $JAppJar))
6 | {
7 | throw "Please build the project using '.\gradlew' first"
8 | }
9 |
10 | java -jar $JAppJar @args
11 | exit $LASTEXITCODE
12 |
--------------------------------------------------------------------------------
/bin/japp.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | project_dir=$(realpath $(dirname $(realpath "$0"))/..)
4 | japp_jar="$project_dir/build/japp.jar"
5 |
6 | if [ ! -f "$japp_jar" ]; then
7 | echo "Please build the project using './gradlew' first" >&2
8 | exit 1
9 | fi
10 |
11 | exec java -jar $japp_jar "$@"
12 |
--------------------------------------------------------------------------------
/boot/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.glavo.mic.tasks.CompileModuleInfo
2 |
3 | plugins {
4 | id("org.glavo.compile-module-info-plugin") version "2.0" apply false
5 | }
6 |
7 | dependencies {
8 | implementation(project(":base"))
9 | }
10 |
11 | tasks.compileJava {
12 | options.compilerArgs.addAll(
13 | listOf(
14 | "--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED",
15 | "--add-exports=java.base/jdk.internal.module=ALL-UNNAMED",
16 | "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED",
17 | )
18 | )
19 | }
20 |
21 | val mainClassName = "org.glavo.japp.boot.JAppBootLauncher"
22 |
23 | val compileModuleInfo = tasks.create("compileModuleInfo") {
24 | sourceFile.set(layout.projectDirectory.file("src/main/module-info/module-info.java"))
25 | targetFile.set(layout.buildDirectory.file("classes/module-info/main/module-info.class"))
26 | moduleMainClass = mainClassName
27 | }
28 |
29 | tasks.classes.get().dependsOn(compileModuleInfo)
30 |
31 | tasks.create("bootJar") {
32 | destinationDirectory.set(rootProject.layout.buildDirectory)
33 | archiveFileName.set("japp-boot.jar")
34 |
35 | dependsOn(configurations.runtimeClasspath)
36 |
37 | from(sourceSets.main.get().output)
38 | from(configurations.runtimeClasspath.get().map { zipTree(it) })
39 | }
40 |
41 | tasks.withType {
42 | from(compileModuleInfo.targetFile)
43 | manifest.attributes("Main-Class" to mainClassName)
44 | }
45 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/JAppBootArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot;
17 |
18 | import java.io.IOException;
19 | import java.nio.ByteBuffer;
20 | import java.nio.file.Path;
21 | import java.util.ArrayList;
22 | import java.util.List;
23 |
24 | public final class JAppBootArgs {
25 | public enum Field {
26 | END,
27 | MAIN_CLASS,
28 | MAIN_MODULE,
29 | MODULE_PATH,
30 | CLASS_PATH,
31 | ADD_READS,
32 | ADD_EXPORTS,
33 | ADD_OPENS,
34 | ENABLE_NATIVE_ACCESS;
35 |
36 | private static final Field[] VALUES = values();
37 |
38 | public static Field readFrom(ByteBuffer buffer) throws IOException {
39 | byte id = buffer.get();
40 | if (id >= 0 && id < VALUES.length) {
41 | return VALUES[id];
42 | }
43 | throw new IOException(String.format("Unknown field: 0x%02x", Byte.toUnsignedInt(id)));
44 | }
45 |
46 | public byte id() {
47 | return (byte) ordinal();
48 | }
49 | }
50 |
51 | public static final byte ID_RESOLVED_REFERENCE_END = 0;
52 | public static final byte ID_RESOLVED_REFERENCE_LOCAL = 1;
53 | public static final byte ID_RESOLVED_REFERENCE_EXTERNAL = 2;
54 |
55 | String mainClass;
56 | String mainModule;
57 |
58 | final List addReads = new ArrayList<>();
59 | final List addExports = new ArrayList<>();
60 | final List addOpens = new ArrayList<>();
61 |
62 | final List enableNativeAccess = new ArrayList<>();
63 |
64 | final List externalModules = new ArrayList<>();
65 | }
66 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/JAppResourceField.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot;
17 |
18 | public enum JAppResourceField {
19 | END,
20 |
21 | /**
22 | * XXH64 Checksum (8byte)
23 | */
24 | CHECKSUM,
25 |
26 | FILE_CREATE_TIME,
27 | FILE_LAST_MODIFIED_TIME,
28 | FILE_LAST_ACCESS_TIME
29 | ;
30 |
31 | private static final JAppResourceField[] FIELDS = values();
32 |
33 | public static JAppResourceField of(int i) {
34 | return i >= 0 && i < FIELDS.length ? FIELDS[i] : null;
35 | }
36 |
37 | public byte id() {
38 | return (byte) ordinal();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/JAppResourceGroup.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot;
17 |
18 | import java.util.LinkedHashMap;
19 |
20 | public final class JAppResourceGroup extends LinkedHashMap {
21 |
22 | public static final byte MAGIC_NUMBER = (byte) 0xeb;
23 | public static final int HEADER_LENGTH = 24; // 1 + 1 + 2 + 4 + 4 + 4 + 8
24 |
25 | private String name;
26 |
27 | public JAppResourceGroup() {
28 | }
29 |
30 | public void initName(String name) {
31 | if (this.name != null) {
32 | throw new IllegalStateException();
33 | }
34 |
35 | this.name = name;
36 | }
37 |
38 | public String getName() {
39 | return name;
40 | }
41 |
42 | @Override
43 | public String toString() {
44 | return getClass().getSimpleName() + super.toString();
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/JAppResourceRoot.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot;
17 |
18 | import java.net.URI;
19 | import java.net.URISyntaxException;
20 | import java.util.Locale;
21 |
22 | public enum JAppResourceRoot {
23 | MODULES, CLASSPATH, RESOURCE;
24 |
25 | private final String rootName;
26 | private final String pathPrefix;
27 |
28 | JAppResourceRoot() {
29 | this.rootName = name().toLowerCase(Locale.ROOT);
30 | this.pathPrefix = '/' + rootName;
31 | }
32 |
33 | public String getRootName() {
34 | return rootName;
35 | }
36 |
37 | public String getPathPrefix() {
38 | return pathPrefix;
39 | }
40 |
41 | public URI toURI(JAppResourceGroup group) {
42 | try {
43 | return new URI("japp", null, pathPrefix + '/' + group.getName() + '/', null);
44 | } catch (URISyntaxException e) {
45 | throw new RuntimeException(e);
46 | }
47 | }
48 |
49 | public URI toURI(JAppResourceGroup group, JAppResource resource) {
50 | try {
51 | return new URI("japp", null, pathPrefix + '/' + group.getName() + '/' + resource.getName(), null);
52 | } catch (URISyntaxException e) {
53 | throw new RuntimeException(e);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/DecompressContext.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.decompressor;
17 |
18 | import org.glavo.japp.boot.decompressor.classfile.ByteArrayPool;
19 |
20 | import java.nio.ByteBuffer;
21 |
22 | public interface DecompressContext {
23 | ByteArrayPool getPool();
24 |
25 | void decompressZstd(ByteBuffer input, ByteBuffer output);
26 | }
27 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/classfile/ByteArrayPool.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.decompressor.classfile;
17 |
18 | import org.glavo.japp.CompressionMethod;
19 | import org.glavo.japp.boot.decompressor.zstd.ZstdFrameDecompressor;
20 |
21 | import java.io.IOException;
22 | import java.nio.ByteBuffer;
23 |
24 | public final class ByteArrayPool {
25 | public static final byte MAGIC_NUMBER = (byte) 0xf0;
26 |
27 | private final byte[] bytes;
28 | private final long[] offsetAndSize;
29 |
30 | private ByteArrayPool(byte[] bytes, long[] offsetAndSize) {
31 | this.bytes = bytes;
32 | this.offsetAndSize = offsetAndSize;
33 | }
34 |
35 | public static ByteArrayPool readFrom(ByteBuffer buffer, ZstdFrameDecompressor decompressor) throws IOException {
36 | byte magic = buffer.get();
37 | if (magic != MAGIC_NUMBER) {
38 | throw new IOException(String.format("Wrong boot magic: 0x%02x", Byte.toUnsignedInt(magic)));
39 | }
40 |
41 | CompressionMethod compressionMethod = CompressionMethod.readFrom(buffer);
42 |
43 | short reserved = buffer.getShort();
44 | if (reserved != 0) {
45 | throw new IOException("Reserved is not zero");
46 | }
47 |
48 | int count = buffer.getInt();
49 | int uncompressedBytesSize = buffer.getInt();
50 | int compressedBytesSize = buffer.getInt();
51 |
52 | long[] offsetAndSize = new long[count];
53 |
54 | int offset = 0;
55 | for (int i = 0; i < count; i++) {
56 | int s = Short.toUnsignedInt(buffer.getShort());
57 | offsetAndSize[i] = (((long) s) << 32) | (long) offset;
58 | offset += s;
59 | }
60 |
61 | byte[] compressedBytes = new byte[compressedBytesSize];
62 | buffer.get(compressedBytes);
63 |
64 | byte[] uncompressedBytes;
65 | if (compressionMethod == CompressionMethod.NONE) {
66 | uncompressedBytes = compressedBytes;
67 | } else {
68 | uncompressedBytes = new byte[uncompressedBytesSize];
69 | if (compressionMethod == CompressionMethod.ZSTD) {
70 | decompressor.decompress(compressedBytes, 0, compressedBytesSize, uncompressedBytes, 0, uncompressedBytesSize);
71 | } else {
72 | throw new IOException("Unsupported compression method: " + compressionMethod);
73 | }
74 | }
75 |
76 | return new ByteArrayPool(uncompressedBytes, offsetAndSize);
77 | }
78 |
79 | public ByteBuffer get(int index) {
80 | long l = offsetAndSize[index];
81 | int offset = (int) (l & 0xffff_ffffL);
82 | int size = (int) (l >>> 32);
83 |
84 | return ByteBuffer.wrap(bytes, offset, size);
85 | }
86 |
87 | public int get(int index, ByteBuffer output) {
88 | long l = offsetAndSize[index];
89 | int offset = (int) (l & 0xffff_ffffL);
90 | int size = (int) (l >>> 32);
91 |
92 | output.put(bytes, offset, size);
93 | return size;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/Constants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed under the Apache License, Version 2.0 (the "License");
3 | * you may not use this file except in compliance with the License.
4 | * You may obtain a copy of the License at
5 | *
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Unless required by applicable law or agreed to in writing, software
9 | * distributed under the License is distributed on an "AS IS" BASIS,
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | * See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 | package org.glavo.japp.boot.decompressor.zstd;
15 |
16 | final class Constants {
17 | public static final int SIZE_OF_BYTE = 1;
18 | public static final int SIZE_OF_SHORT = 2;
19 | public static final int SIZE_OF_INT = 4;
20 | public static final int SIZE_OF_LONG = 8;
21 |
22 | public static final int MAGIC_NUMBER = 0xFD2FB528;
23 |
24 | public static final int MIN_WINDOW_LOG = 10;
25 | public static final int MAX_WINDOW_LOG = 31;
26 |
27 | public static final int SIZE_OF_BLOCK_HEADER = 3;
28 |
29 | public static final int MIN_SEQUENCES_SIZE = 1;
30 | public static final int MIN_BLOCK_SIZE = 1 // block type tag
31 | + 1 // min size of raw or rle length header
32 | + MIN_SEQUENCES_SIZE;
33 | public static final int MAX_BLOCK_SIZE = 128 * 1024;
34 |
35 | public static final int REPEATED_OFFSET_COUNT = 3;
36 |
37 | // block types
38 | public static final int RAW_BLOCK = 0;
39 | public static final int RLE_BLOCK = 1;
40 | public static final int COMPRESSED_BLOCK = 2;
41 |
42 | // sequence encoding types
43 | public static final int SEQUENCE_ENCODING_BASIC = 0;
44 | public static final int SEQUENCE_ENCODING_RLE = 1;
45 | public static final int SEQUENCE_ENCODING_COMPRESSED = 2;
46 | public static final int SEQUENCE_ENCODING_REPEAT = 3;
47 |
48 | public static final int MAX_LITERALS_LENGTH_SYMBOL = 35;
49 | public static final int MAX_MATCH_LENGTH_SYMBOL = 52;
50 | public static final int MAX_OFFSET_CODE_SYMBOL = 31;
51 | public static final int DEFAULT_MAX_OFFSET_CODE_SYMBOL = 28;
52 |
53 | public static final int LITERAL_LENGTH_TABLE_LOG = 9;
54 | public static final int MATCH_LENGTH_TABLE_LOG = 9;
55 | public static final int OFFSET_TABLE_LOG = 8;
56 |
57 | // literal block types
58 | public static final int RAW_LITERALS_BLOCK = 0;
59 | public static final int RLE_LITERALS_BLOCK = 1;
60 | public static final int COMPRESSED_LITERALS_BLOCK = 2;
61 | public static final int TREELESS_LITERALS_BLOCK = 3;
62 |
63 | public static final int LONG_NUMBER_OF_SEQUENCES = 0x7F00;
64 |
65 | public static final int[] LITERALS_LENGTH_BITS = {0, 0, 0, 0, 0, 0, 0, 0,
66 | 0, 0, 0, 0, 0, 0, 0, 0,
67 | 1, 1, 1, 1, 2, 2, 3, 3,
68 | 4, 6, 7, 8, 9, 10, 11, 12,
69 | 13, 14, 15, 16};
70 |
71 | public static final int[] MATCH_LENGTH_BITS = {0, 0, 0, 0, 0, 0, 0, 0,
72 | 0, 0, 0, 0, 0, 0, 0, 0,
73 | 0, 0, 0, 0, 0, 0, 0, 0,
74 | 0, 0, 0, 0, 0, 0, 0, 0,
75 | 1, 1, 1, 1, 2, 2, 3, 3,
76 | 4, 4, 5, 7, 8, 9, 10, 11,
77 | 12, 13, 14, 15, 16};
78 |
79 | private Constants() {
80 | }
81 | }
82 |
83 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/FrameHeader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed under the Apache License, Version 2.0 (the "License");
3 | * you may not use this file except in compliance with the License.
4 | * You may obtain a copy of the License at
5 | *
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Unless required by applicable law or agreed to in writing, software
9 | * distributed under the License is distributed on an "AS IS" BASIS,
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | * See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 | package org.glavo.japp.boot.decompressor.zstd;
15 |
16 | import java.util.Objects;
17 | import java.util.StringJoiner;
18 |
19 | import static org.glavo.japp.boot.decompressor.zstd.Util.checkState;
20 |
21 | final class FrameHeader {
22 | final long headerSize;
23 | final int windowSize;
24 | final long contentSize;
25 | final long dictionaryId;
26 | final boolean hasChecksum;
27 |
28 | public FrameHeader(long headerSize, int windowSize, long contentSize, long dictionaryId, boolean hasChecksum) {
29 | checkState(windowSize >= 0 || contentSize >= 0, "Invalid frame header: contentSize or windowSize must be set");
30 | this.headerSize = headerSize;
31 | this.windowSize = windowSize;
32 | this.contentSize = contentSize;
33 | this.dictionaryId = dictionaryId;
34 | this.hasChecksum = hasChecksum;
35 | }
36 |
37 | @Override
38 | public boolean equals(Object o) {
39 | if (this == o) {
40 | return true;
41 | }
42 | if (o == null || getClass() != o.getClass()) {
43 | return false;
44 | }
45 | FrameHeader that = (FrameHeader) o;
46 | return headerSize == that.headerSize &&
47 | windowSize == that.windowSize &&
48 | contentSize == that.contentSize &&
49 | dictionaryId == that.dictionaryId &&
50 | hasChecksum == that.hasChecksum;
51 | }
52 |
53 | @Override
54 | public int hashCode() {
55 | return Objects.hash(headerSize, windowSize, contentSize, dictionaryId, hasChecksum);
56 | }
57 |
58 | @Override
59 | public String toString() {
60 | return new StringJoiner(", ", FrameHeader.class.getSimpleName() + "[", "]")
61 | .add("headerSize=" + headerSize)
62 | .add("windowSize=" + windowSize)
63 | .add("contentSize=" + contentSize)
64 | .add("dictionaryId=" + dictionaryId)
65 | .add("hasChecksum=" + hasChecksum)
66 | .toString();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/FseCompressionTable.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed under the Apache License, Version 2.0 (the "License");
3 | * you may not use this file except in compliance with the License.
4 | * You may obtain a copy of the License at
5 | *
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Unless required by applicable law or agreed to in writing, software
9 | * distributed under the License is distributed on an "AS IS" BASIS,
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | * See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 | package org.glavo.japp.boot.decompressor.zstd;
15 |
16 | final class FseCompressionTable {
17 | private FseCompressionTable() {
18 | }
19 |
20 | private static int calculateStep(int tableSize) {
21 | return (tableSize >>> 1) + (tableSize >>> 3) + 3;
22 | }
23 |
24 | public static int spreadSymbols(short[] normalizedCounters, int maxSymbolValue, int tableSize, int highThreshold, byte[] symbols) {
25 | int mask = tableSize - 1;
26 | int step = calculateStep(tableSize);
27 |
28 | int position = 0;
29 | for (byte symbol = 0; symbol <= maxSymbolValue; symbol++) {
30 | for (int i = 0; i < normalizedCounters[symbol]; i++) {
31 | symbols[position] = symbol;
32 | do {
33 | position = (position + step) & mask;
34 | }
35 | while (position > highThreshold);
36 | }
37 | }
38 | return position;
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/MalformedInputException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed under the Apache License, Version 2.0 (the "License");
3 | * you may not use this file except in compliance with the License.
4 | * You may obtain a copy of the License at
5 | *
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Unless required by applicable law or agreed to in writing, software
9 | * distributed under the License is distributed on an "AS IS" BASIS,
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | * See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 | package org.glavo.japp.boot.decompressor.zstd;
15 |
16 | public class MalformedInputException extends RuntimeException {
17 | private final long offset;
18 |
19 | public MalformedInputException(long offset) {
20 | this(offset, "Malformed input");
21 | }
22 |
23 | public MalformedInputException(long offset, String reason) {
24 | super(reason + ": offset=" + offset);
25 | this.offset = offset;
26 | }
27 |
28 | public long getOffset() {
29 | return offset;
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/Util.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed under the Apache License, Version 2.0 (the "License");
3 | * you may not use this file except in compliance with the License.
4 | * You may obtain a copy of the License at
5 | *
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Unless required by applicable law or agreed to in writing, software
9 | * distributed under the License is distributed on an "AS IS" BASIS,
10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | * See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 | package org.glavo.japp.boot.decompressor.zstd;
15 |
16 | import org.glavo.japp.util.MemoryAccess;
17 |
18 | import static org.glavo.japp.boot.decompressor.zstd.Constants.SIZE_OF_SHORT;
19 |
20 | final class Util {
21 | private Util() {
22 | }
23 |
24 | public static int highestBit(int value) {
25 | return 31 - Integer.numberOfLeadingZeros(value);
26 | }
27 |
28 | public static boolean isPowerOf2(int value) {
29 | return (value & (value - 1)) == 0;
30 | }
31 |
32 | public static int mask(int bits) {
33 | return (1 << bits) - 1;
34 | }
35 |
36 | public static void verify(boolean condition, long offset, String reason) {
37 | if (!condition) {
38 | throw new MalformedInputException(offset, reason);
39 | }
40 | }
41 |
42 | public static void checkArgument(boolean condition, String reason) {
43 | if (!condition) {
44 | throw new IllegalArgumentException(reason);
45 | }
46 | }
47 |
48 | static void checkPositionIndexes(int start, int end, int size) {
49 | // Carefully optimized for execution by hotspot (explanatory comment above)
50 | if (start < 0 || end < start || end > size) {
51 | throw new IndexOutOfBoundsException(badPositionIndexes(start, end, size));
52 | }
53 | }
54 |
55 | private static String badPositionIndexes(int start, int end, int size) {
56 | if (start < 0 || start > size) {
57 | return badPositionIndex(start, size, "start index");
58 | }
59 | if (end < 0 || end > size) {
60 | return badPositionIndex(end, size, "end index");
61 | }
62 | // end < start
63 | return String.format("end index (%s) must not be less than start index (%s)", end, start);
64 | }
65 |
66 | private static String badPositionIndex(int index, int size, String desc) {
67 | if (index < 0) {
68 | return String.format("%s (%s) must not be negative", desc, index);
69 | } else if (size < 0) {
70 | throw new IllegalArgumentException("negative size: " + size);
71 | } else { // index > size
72 | return String.format("%s (%s) must not be greater than size (%s)", desc, index, size);
73 | }
74 | }
75 |
76 | public static void checkState(boolean condition, String reason) {
77 | if (!condition) {
78 | throw new IllegalStateException(reason);
79 | }
80 | }
81 |
82 | public static MalformedInputException fail(long offset, String reason) {
83 | throw new MalformedInputException(offset, reason);
84 | }
85 |
86 | public static int get24BitLittleEndian(Object inputBase, long inputAddress) {
87 | return (MemoryAccess.getShort(inputBase, inputAddress) & 0xFFFF)
88 | | ((MemoryAccess.getByte(inputBase, inputAddress + SIZE_OF_SHORT) & 0xFF) << Short.SIZE);
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/decompressor/zstd/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Based on aircompressor (https://github.com/airlift/aircompressor)
3 | */
4 | package org.glavo.japp.boot.decompressor.zstd;
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/jappfs/JAppDirectoryStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.jappfs;
17 |
18 | import java.io.IOException;
19 | import java.nio.file.ClosedDirectoryStreamException;
20 | import java.nio.file.DirectoryStream;
21 | import java.nio.file.Path;
22 | import java.util.Iterator;
23 | import java.util.NoSuchElementException;
24 |
25 | public final class JAppDirectoryStream implements DirectoryStream {
26 |
27 | private final JAppPath path;
28 | private final JAppFileSystem.DirectoryNode> node;
29 | private final DirectoryStream.Filter super Path> filter;
30 | private Itr itr;
31 |
32 | private boolean isClosed = false;
33 |
34 | public JAppDirectoryStream(JAppPath path, JAppFileSystem.DirectoryNode> node, Filter super Path> filter) {
35 | this.path = path;
36 | this.filter = filter;
37 | this.node = node;
38 | }
39 |
40 | @Override
41 | public Iterator iterator() {
42 | if (isClosed) {
43 | throw new ClosedDirectoryStreamException();
44 | }
45 |
46 | if (itr != null) {
47 | throw new IllegalStateException("Iterator has already been returned");
48 | }
49 |
50 | return itr = new Itr();
51 | }
52 |
53 | @Override
54 | public void close() throws IOException {
55 | isClosed = true;
56 | }
57 |
58 | private final class Itr implements Iterator {
59 | private final Iterator extends JAppFileSystem.Node> nodeIterator = node.getChildren().iterator();
60 | private Path nextPath;
61 |
62 | @Override
63 | public boolean hasNext() {
64 | if (isClosed) {
65 | return false;
66 | }
67 |
68 | if (nextPath != null) {
69 | return true;
70 | }
71 |
72 | while (nodeIterator.hasNext()) {
73 | JAppFileSystem.Node node = nodeIterator.next();
74 | JAppPath p = (JAppPath) path.resolve(node.getName());
75 | try {
76 | if (filter == null || filter.accept(p)) {
77 | nextPath = p;
78 | return true;
79 | }
80 | } catch (IOException ignored) {
81 | }
82 | }
83 | return false;
84 | }
85 |
86 | @Override
87 | public Path next() {
88 | if (hasNext()) {
89 | Path p = nextPath;
90 | nextPath = null;
91 | return p;
92 | } else {
93 | throw new NoSuchElementException();
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/jappfs/JAppFileAttributeView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.jappfs;
17 |
18 | import java.io.IOException;
19 | import java.nio.file.LinkOption;
20 | import java.nio.file.NoSuchFileException;
21 | import java.nio.file.ReadOnlyFileSystemException;
22 | import java.nio.file.attribute.BasicFileAttributeView;
23 | import java.nio.file.attribute.FileTime;
24 |
25 | public final class JAppFileAttributeView implements BasicFileAttributeView {
26 |
27 | private final JAppPath path;
28 | private final boolean isJAppView;
29 | private final LinkOption[] options;
30 |
31 | public JAppFileAttributeView(JAppPath path, boolean isJAppView, LinkOption... options) {
32 | this.path = path;
33 | this.isJAppView = isJAppView;
34 | this.options = options;
35 | }
36 |
37 | @Override
38 | public String name() {
39 | return isJAppView ? "japp" : "basic";
40 | }
41 |
42 | @Override
43 | public JAppFileAttributes readAttributes() throws IOException {
44 | JAppFileSystem fileSystem = path.getFileSystem();
45 | JAppFileSystem.Node node = fileSystem.resolve(path);
46 | if (node == null) {
47 | throw new NoSuchFileException(path.toString());
48 | }
49 | return new JAppFileAttributes(fileSystem, node);
50 | }
51 |
52 | @Override
53 | public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
54 | throw new ReadOnlyFileSystemException();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/jappfs/JAppFileAttributes.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.jappfs;
17 |
18 | import java.nio.file.attribute.BasicFileAttributes;
19 | import java.nio.file.attribute.FileTime;
20 |
21 | public class JAppFileAttributes implements BasicFileAttributes {
22 |
23 | enum Attribute {
24 | // basic
25 | size,
26 | creationTime,
27 | lastAccessTime,
28 | lastModifiedTime,
29 | isDirectory,
30 | isRegularFile,
31 | isSymbolicLink,
32 | isOther,
33 | fileKey,
34 |
35 | // japp
36 | compressedSize
37 | }
38 |
39 | private final JAppFileSystem fileSystem;
40 | private final JAppFileSystem.Node node;
41 |
42 | public JAppFileAttributes(JAppFileSystem fileSystem, JAppFileSystem.Node node) {
43 | this.fileSystem = fileSystem;
44 | this.node = node;
45 | }
46 |
47 | private FileTime getDefaultFileTime() {
48 | return FileTime.fromMillis(0);
49 | }
50 |
51 | Object getAttribute(Attribute attribute) {
52 | switch (attribute) {
53 | case size:
54 | return size();
55 | case creationTime:
56 | return creationTime();
57 | case lastAccessTime:
58 | return lastAccessTime();
59 | case lastModifiedTime:
60 | return lastModifiedTime();
61 | case isDirectory:
62 | return isDirectory();
63 | case isRegularFile:
64 | return isRegularFile();
65 | case isSymbolicLink:
66 | return isSymbolicLink();
67 | case isOther:
68 | return isOther();
69 | case fileKey:
70 | return fileKey();
71 | case compressedSize:
72 | return compressedSize();
73 | default:
74 | return null;
75 | }
76 | }
77 |
78 | @Override
79 | public FileTime lastModifiedTime() {
80 | if (node instanceof JAppFileSystem.ResourceNode) {
81 | return ((JAppFileSystem.ResourceNode) node).getResource().getLastModifiedTime();
82 | } else {
83 | return getDefaultFileTime();
84 | }
85 | }
86 |
87 | @Override
88 | public FileTime lastAccessTime() {
89 | if (node instanceof JAppFileSystem.ResourceNode) {
90 | return ((JAppFileSystem.ResourceNode) node).getResource().getLastAccessTime();
91 | } else {
92 | return getDefaultFileTime();
93 | }
94 | }
95 |
96 | @Override
97 | public FileTime creationTime() {
98 | if (node instanceof JAppFileSystem.ResourceNode) {
99 | return ((JAppFileSystem.ResourceNode) node).getResource().getCreationTime();
100 | } else {
101 | return getDefaultFileTime();
102 | }
103 | }
104 |
105 | @Override
106 | public boolean isRegularFile() {
107 | return node instanceof JAppFileSystem.ResourceNode;
108 | }
109 |
110 | @Override
111 | public boolean isDirectory() {
112 | return node instanceof JAppFileSystem.DirectoryNode;
113 | }
114 |
115 | @Override
116 | public boolean isSymbolicLink() {
117 | return false;
118 | }
119 |
120 | @Override
121 | public boolean isOther() {
122 | return false;
123 | }
124 |
125 | @Override
126 | public long size() {
127 | return (node instanceof JAppFileSystem.ResourceNode) ? ((JAppFileSystem.ResourceNode) node).getResource().getSize() : 0L;
128 | }
129 |
130 | @Override
131 | public Object fileKey() {
132 | return node;
133 | }
134 |
135 | // JApp
136 |
137 | public long compressedSize() {
138 | return (node instanceof JAppFileSystem.ResourceNode) ? ((JAppFileSystem.ResourceNode) node).getResource().getCompressedSize() : 0L;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/jappfs/JAppFileStore.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.jappfs;
17 |
18 | import java.io.IOException;
19 | import java.nio.file.FileStore;
20 | import java.nio.file.attribute.BasicFileAttributeView;
21 | import java.nio.file.attribute.FileAttributeView;
22 | import java.nio.file.attribute.FileStoreAttributeView;
23 | import java.util.Objects;
24 |
25 | public final class JAppFileStore extends FileStore {
26 |
27 | private final JAppFileSystem fs;
28 |
29 | JAppFileStore(JAppFileSystem fs) {
30 | this.fs = fs;
31 | }
32 |
33 | @Override
34 | public String name() {
35 | return fs + "/";
36 | }
37 |
38 | @Override
39 | public String type() {
40 | return "jappfs";
41 | }
42 |
43 | @Override
44 | public boolean isReadOnly() {
45 | return true;
46 | }
47 |
48 | @Override
49 | public boolean supportsFileAttributeView(String name) {
50 | return name.equals("basic");
51 | }
52 |
53 | @Override
54 | public boolean supportsFileAttributeView(Class extends FileAttributeView> type) {
55 | return type == BasicFileAttributeView.class;
56 | }
57 |
58 | @Override
59 | public V getFileStoreAttributeView(Class type) {
60 | Objects.requireNonNull(type);
61 | return null;
62 | }
63 |
64 | @Override
65 | public long getTotalSpace() throws IOException {
66 | throw new UnsupportedOperationException("getTotalSpace");
67 | }
68 |
69 | @Override
70 | public long getUsableSpace() throws IOException {
71 | return 0L;
72 | }
73 |
74 | @Override
75 | public long getUnallocatedSpace() throws IOException {
76 | return 0L;
77 | }
78 |
79 | @Override
80 | public Object getAttribute(String attribute) throws IOException {
81 | throw new UnsupportedOperationException("does not support " + attribute);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/module/JAppModuleReference.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.module;
17 |
18 | import org.glavo.japp.boot.JAppResourceGroup;
19 | import org.glavo.japp.boot.JAppReader;
20 | import org.glavo.japp.boot.JAppResource;
21 | import org.glavo.japp.boot.JAppResourceRoot;
22 |
23 | import java.io.IOException;
24 | import java.io.InputStream;
25 | import java.lang.module.ModuleDescriptor;
26 | import java.lang.module.ModuleReader;
27 | import java.lang.module.ModuleReference;
28 | import java.net.URI;
29 | import java.nio.ByteBuffer;
30 | import java.util.Optional;
31 | import java.util.stream.Stream;
32 |
33 | public final class JAppModuleReference extends ModuleReference implements ModuleReader {
34 |
35 | private final JAppReader reader;
36 | private final JAppResourceGroup group;
37 |
38 | public JAppModuleReference(JAppReader reader, ModuleDescriptor descriptor, JAppResourceGroup group) {
39 | super(descriptor, JAppResourceRoot.MODULES.toURI(group));
40 |
41 | this.reader = reader;
42 | this.group = group;
43 | }
44 |
45 | @Override
46 | public ModuleReader open() throws IOException {
47 | return this;
48 | }
49 |
50 | // ModuleReader
51 |
52 | @Override
53 | public Optional find(String name) throws IOException {
54 | JAppResource resource = group.get(name);
55 | if (resource != null) {
56 | return Optional.of(JAppResourceRoot.MODULES.toURI(group, resource));
57 | } else {
58 | return Optional.empty();
59 | }
60 | }
61 |
62 | @Override
63 | public Optional open(String name) throws IOException {
64 | JAppResource resource = group.get(name);
65 | if (resource != null) {
66 | return Optional.of(reader.openResource(resource));
67 | } else {
68 | return Optional.empty();
69 | }
70 | }
71 |
72 | @Override
73 | public Optional read(String name) throws IOException {
74 | JAppResource resource = group.get(name);
75 | if (resource != null) {
76 | return Optional.of(reader.readResource(resource));
77 | } else {
78 | return Optional.empty();
79 | }
80 | }
81 |
82 | @Override
83 | public Stream list() throws IOException {
84 | return group.values().stream().map(JAppResource::getName);
85 | }
86 |
87 | @Override
88 | public void close() throws IOException {
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/url/JAppURLConnection.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.url;
17 |
18 | import org.glavo.japp.boot.JAppReader;
19 | import org.glavo.japp.boot.JAppResource;
20 | import org.glavo.japp.boot.JAppResourceRoot;
21 |
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import java.net.MalformedURLException;
25 | import java.net.URL;
26 | import java.net.URLConnection;
27 |
28 | public final class JAppURLConnection extends URLConnection {
29 |
30 | private final JAppReader reader;
31 |
32 | private JAppResource resource;
33 |
34 | private final JAppResourceRoot root;
35 | private final String group;
36 | private final String path;
37 |
38 | JAppURLConnection(JAppReader reader, URL url) throws MalformedURLException {
39 | super(url);
40 |
41 | this.reader = reader;
42 |
43 | String fullPath = url.getPath();
44 | if (!fullPath.startsWith("/")) {
45 | throw invalidURL();
46 | }
47 |
48 | JAppResourceRoot root = null;
49 | String group = null;
50 | String path = null;
51 | for (JAppResourceRoot r : JAppResourceRoot.values()) {
52 | String prefix = r.getPathPrefix();
53 | if (fullPath.startsWith(prefix) && fullPath.length() > prefix.length() && fullPath.charAt(prefix.length()) == '/') {
54 | int idx = fullPath.indexOf('/', prefix.length() + 1);
55 | if (idx < 0) {
56 | throw invalidURL();
57 | }
58 |
59 | root = r;
60 | group = fullPath.substring(prefix.length() + 1, idx);
61 | path = fullPath.substring(idx + 1);
62 | break;
63 | }
64 | }
65 |
66 | if (root == null) {
67 | throw invalidURL();
68 | }
69 |
70 | this.root = root;
71 | this.group = group;
72 | this.path = path;
73 | }
74 |
75 | private MalformedURLException invalidURL() {
76 | return new MalformedURLException("Invalid URL: " + this.url);
77 | }
78 |
79 | @Override
80 | public void connect() throws IOException {
81 | if (connected) {
82 | return;
83 | }
84 |
85 | resource = reader.findResource(root, group, path);
86 | if (resource == null) {
87 | throw new IOException("Resource not found");
88 | }
89 |
90 | connected = true;
91 | }
92 |
93 | @Override
94 | public InputStream getInputStream() throws IOException {
95 | connect();
96 | return reader.openResource(resource);
97 | }
98 |
99 | @Override
100 | public long getContentLengthLong() {
101 | try {
102 | connect();
103 | return resource.getSize();
104 | } catch (IOException ignored) {
105 | return -1L;
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/url/JAppURLHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.url;
17 |
18 | import org.glavo.japp.boot.JAppReader;
19 |
20 | import java.io.IOException;
21 | import java.net.URL;
22 | import java.net.URLConnection;
23 | import java.net.URLStreamHandler;
24 |
25 | public class JAppURLHandler extends URLStreamHandler {
26 | @Override
27 | protected URLConnection openConnection(URL u) throws IOException {
28 | return new JAppURLConnection(JAppReader.getSystemReader(), u);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/url/JAppURLStreamHandlerFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.url;
17 |
18 | import java.net.URL;
19 | import java.net.URLStreamHandler;
20 | import java.net.URLStreamHandlerFactory;
21 | import java.util.*;
22 |
23 | // For Java 8
24 | public class JAppURLStreamHandlerFactory implements URLStreamHandlerFactory {
25 |
26 | private static final String DEFAULT_PREFIX = "sun.net.www.protocol";
27 |
28 | public static void setup() {
29 | URL.setURLStreamHandlerFactory(new JAppURLStreamHandlerFactory(System.getProperty("java.protocol.handler.pkgs")));
30 | }
31 |
32 | private final List packagePrefixes;
33 | private final Map handles = new HashMap<>();
34 |
35 | public JAppURLStreamHandlerFactory(String packagePrefixList) {
36 | handles.put("japp", new JAppURLHandler());
37 |
38 | if (packagePrefixList == null) {
39 | this.packagePrefixes = Collections.singletonList(DEFAULT_PREFIX);
40 | } else {
41 | Set prefixes = new LinkedHashSet<>();
42 |
43 | StringTokenizer packagePrefixIter =
44 | new StringTokenizer(packagePrefixList, "|");
45 |
46 | while (packagePrefixIter.hasMoreTokens()) {
47 | prefixes.add(packagePrefixIter.nextToken().trim());
48 | }
49 |
50 | prefixes.add(DEFAULT_PREFIX);
51 |
52 | this.packagePrefixes = Arrays.asList(prefixes.toArray(new String[0]));
53 | }
54 | }
55 |
56 | @SuppressWarnings("deprecation")
57 | @Override
58 | public URLStreamHandler createURLStreamHandler(String protocol) {
59 | // Avoid using reflection during bootstrap
60 |
61 | URLStreamHandler handler = handles.get(protocol);
62 | if (handler != null) {
63 | return handler;
64 | }
65 |
66 | synchronized (this) {
67 | handler = handles.get(protocol);
68 | if (handler != null) {
69 | return handler;
70 | }
71 |
72 |
73 | for (String prefix : packagePrefixes) {
74 | String name = prefix + protocol + ".Handler";
75 | try {
76 | Class> cls = Class.forName(name);
77 | handler = (URLStreamHandler) cls.newInstance();
78 | break;
79 | } catch (Exception ignored) {
80 | }
81 | }
82 |
83 | if (handler != null) {
84 | handles.put(protocol, handler);
85 | }
86 | }
87 |
88 | return handler;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/boot/src/main/java/org/glavo/japp/boot/url/JAppURLStreamHandlerProvider.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.url;
17 |
18 | import java.net.URLStreamHandler;
19 | import java.net.spi.URLStreamHandlerProvider;
20 |
21 | // For Java 9+
22 | public class JAppURLStreamHandlerProvider extends URLStreamHandlerProvider {
23 | @Override
24 | public URLStreamHandler createURLStreamHandler(String protocol) {
25 | if ("japp".equals(protocol)) {
26 | return new JAppURLHandler();
27 | }
28 | return null;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/boot/src/main/module-info/module-info.java:
--------------------------------------------------------------------------------
1 | module org.glavo.japp.boot {
2 | provides java.net.spi.URLStreamHandlerProvider
3 | with org.glavo.japp.boot.url.JAppURLStreamHandlerProvider;
4 | provides java.nio.file.spi.FileSystemProvider
5 | with org.glavo.japp.boot.jappfs.JAppFileSystemProvider;
6 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | plugins {
18 | id("java")
19 | id("com.github.johnrengelman.shadow") version "8.1.1"
20 | }
21 |
22 | allprojects {
23 | apply {
24 | plugin("java")
25 | }
26 |
27 | group = "org.glavo"
28 | version = "0.1.0-SNAPSHOT"
29 |
30 | repositories {
31 | mavenCentral()
32 | }
33 |
34 | tasks.compileJava {
35 | // TODO: Java 8
36 | sourceCompatibility = "9"
37 | targetCompatibility = "9"
38 | }
39 | }
40 |
41 | tasks.create("buildAll") {
42 | dependsOn(
43 | ":boot:bootJar", ":shadowJar"
44 | )
45 | }
46 |
47 | defaultTasks("buildAll")
48 |
49 | dependencies {
50 | testImplementation(platform("org.junit:junit-bom:5.10.2"))
51 | testImplementation("org.junit.jupiter:junit-jupiter")
52 |
53 | testImplementation(libs.zstd.jni)
54 |
55 | LWJGL.addDependency(this, "testImplementation", "lwjgl")
56 | LWJGL.addDependency(this, "testImplementation", "lwjgl-xxhash")
57 | }
58 |
59 | tasks.compileTestJava {
60 | options.compilerArgs.add("--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED")
61 | }
62 |
63 | tasks.test {
64 | dependsOn(
65 | "buildAll",
66 | ":test-case:HelloWorld:jar",
67 | ":test-case:ModulePath:jar",
68 | )
69 |
70 | useJUnitPlatform()
71 | jvmArgs(
72 | "--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED"
73 | )
74 |
75 | fun jarPath(projectName: String) =
76 | project(projectName).tasks.getByName("jar").archiveFile.get().asFile.absolutePath
77 |
78 | fun testCase(projectName: String): String {
79 | val p = project(projectName)
80 |
81 | return p.configurations.runtimeClasspath.get().map { it.absolutePath }
82 | .plus(p.tasks.getByName("jar").archiveFile.get().asFile.absolutePath)
83 | .joinToString(File.pathSeparator)
84 | }
85 |
86 | systemProperties(
87 | "japp.jar" to tasks.getByName("shadowJar").archiveFile.get().asFile.absolutePath,
88 | "japp.testcase.helloworld" to testCase(":test-case:HelloWorld"),
89 | "japp.testcase.modulepath" to testCase(":test-case:ModulePath"),
90 | )
91 |
92 | testLogging {
93 | this.showStandardStreams = true
94 | }
95 | }
96 |
97 | dependencies {
98 | implementation(project(":base"))
99 | implementation(project(":boot"))
100 | implementation(libs.zstd.jni)
101 | }
102 |
103 | tasks.jar {
104 | manifest.attributes(
105 | "Main-Class" to "org.glavo.japp.Main",
106 | "Add-Exports" to "java.base/jdk.internal.misc"
107 | )
108 | }
109 |
110 | tasks.shadowJar {
111 | destinationDirectory.set(rootProject.layout.buildDirectory)
112 | archiveFileName.set("japp.jar")
113 | }
114 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Glavo/japp/f288a5458a50d22714b294fb3a893ac7bf1d1dff/buildSrc/build.gradle.kts
--------------------------------------------------------------------------------
/buildSrc/src/main/java/LWJGL.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import org.gradle.api.artifacts.dsl.DependencyHandler;
18 |
19 | import java.util.Locale;
20 |
21 | public class LWJGL {
22 | private static final String LWJGL_VERSION = "3.3.4";
23 | private static final String LWJGL_PLATFORM;
24 |
25 | static {
26 | String lwjglPlatform;
27 |
28 | String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT);
29 | String archName = System.getProperty("os.arch").toLowerCase(Locale.ROOT);
30 | if (osName.startsWith("win")) {
31 | lwjglPlatform = "windows";
32 | } else if (osName.contains("darwin") || osName.contains("mac") || osName.contains("osx")) {
33 | lwjglPlatform = "macos";
34 | } else {
35 | lwjglPlatform = "linux";
36 | }
37 |
38 | switch (archName) {
39 | case "x8664":
40 | case "x86-64":
41 | case "x86_64":
42 | case "amd64":
43 | case "ia32e":
44 | case "em64t":
45 | case "x64":
46 | break;
47 | case "x86":
48 | case "i386":
49 | case "i486":
50 | case "i586":
51 | case "i686":
52 | lwjglPlatform += "-x86";
53 | break;
54 | case "arm64":
55 | case "aarch64":
56 | lwjglPlatform += "-arm64";
57 | break;
58 | case "arm":
59 | case "arm32":
60 | lwjglPlatform += "-arm32";
61 | break;
62 | case "riscv64":
63 | lwjglPlatform += "-riscv64";
64 | break;
65 | case "ppc64le":
66 | case "powerpc64le":
67 | lwjglPlatform += "-ppc64le";
68 | break;
69 | default:
70 | if (archName.startsWith("armv7")) {
71 | lwjglPlatform += "-arm32";
72 | } else if (archName.startsWith("armv8") || archName.startsWith("armv9")) {
73 | lwjglPlatform += "-arm64";
74 | }
75 | }
76 |
77 | LWJGL_PLATFORM = lwjglPlatform;
78 | }
79 |
80 | public static void addDependency(DependencyHandler handler, String configurationName, String moduleName) {
81 | handler.add(configurationName, "org.lwjgl:" + moduleName + ":" + LWJGL_VERSION);
82 | handler.add(configurationName, "org.lwjgl:" + moduleName + ":" + LWJGL_VERSION + ":natives-" + LWJGL_PLATFORM);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/docs/COMPARE.md:
--------------------------------------------------------------------------------
1 | # What's wrong with the existing packaging format?
2 |
3 | For Java programs, we now have many ways to package them, each with different advantages and disadvantages:
4 |
5 | ## jlink/jpackage
6 |
7 | This is the packaging method currently promoted by OpenJDK officials.
8 |
9 | jlink trims the JDK to retain only the required modules, and then packages the JDK and the program together.
10 |
11 | Advantage:
12 |
13 | * The program comes with a Java runtime environment, so users do not need to install additional dependencies;
14 | * Developers can choose the Java runtime to use, avoiding compatibility issues as much as possible.
15 |
16 | Disadvantage:
17 |
18 | * jlink compromises the cross-platform capabilities of Java programs.
19 | This makes it more difficult for users other than Windows/Linux/macOS systems and x86/ARM architectures
20 | (such as Linux RISC-V/LoongArch and AIX users) to use Java programs.
21 |
22 | Most Java programs are actually perfectly cross-platform and can run on any platform that supports the JVM.
23 | However, jlink packages the program with the native files of a specific platform,
24 | so that the packaged program can only run on one platform.
25 |
26 | Although jlink supports cross-building and can package program for other platforms.
27 | However, this is different from Golang, which can easily generate small executable files for different platforms.
28 | jlink needs to download a JDK for each target platform,
29 | and the packaged program ranges from tens of megabytes to hundreds of megabytes.
30 |
31 | For each additional target platform,
32 | an additional 200MB of JDK must be downloaded to the packaging device,
33 | the packaging time will increase by tens of seconds to several minutes,
34 | and finally an additional file of tens to hundreds of MB must be distributed.
35 | This reduces the incentive for developers to provide distribution to more platforms.
36 |
37 | Another thing to consider is, where do we download these JDKs from?
38 | Most developers choose a vendor they trust (such as Eclipse Adoptium, BellSoft, and Azul)
39 | and download all JDKs from him.
40 | This means that the compatibility of the programs they distribute often depends on this vendor.
41 |
42 | The platforms supported by these vendors cover most of the common platforms,
43 | but there are some niche platforms that are not taken care of.
44 | For example, platforms like FreeBSD, Alpine Linux, and Linux LoongArch 64, few JDK vendors considers them,
45 | and Java on these platforms is often provided by the system's package manager.
46 | Therefore, these platforms are rarely considered by developers who use jlink to package programs.
47 | * Since the program is always distributed with the Java runtime environment,
48 | this greatly increases the size of the programs
49 | and the Java runtime can no longer be shared between multiple Java programs.
50 |
51 | For end users, the countless chromium on the disk has already troubled many people,
52 | and jlink has brings countless Java standard libraries/JVMs to their disks.
53 |
54 | For servers, the Java runtime environment does not need to change often.
55 | Traditionally, the Java runtime environment can be installed on the system, or in a base image.
56 | If you want to use jlink, you have to transfer the entire Java standard library/JVM for every update,
57 | which is a complete waste.
58 | * Currently, jlink packages a zip archive instead of a single executable file.
59 | Users need to find a place to unzip/install the program before they can use it,
60 | which is not so convenient and flexible.
61 |
62 | The [Hermetic Java](https://cr.openjdk.org/~jiangli/hermetic_java.pdf) project plans to address this issue,
63 | but it is unknown when it will be completed.
64 |
65 | ## Shadow(Fat) JAR
66 |
67 | Shadow(Fat) JAR technology packages the class files and resources of a Java program and all its dependencies into a single JAR file.
68 |
69 | Advantage:
70 |
71 | * Single file, small, cross-platform, and fast to package.
72 |
73 | Disadvantage:
74 |
75 | * Users need to install Java to run it;
76 | * It cannot choose which Java to start itself with;
77 | * It is not possible to pass JVM options to the JVM and only provides limited control over the JVM through the manifest file;
78 | * Since a JAR file can contain only one module, it does not work well with JPMS and often only works with the classpath.
--------------------------------------------------------------------------------
/docs/introduce.md:
--------------------------------------------------------------------------------
1 | Hi everyone, I'm working on a more modern packaging format for Java programs to replace shadow(fat) jars.
2 |
3 | I already have a prototype and hope to get more people to discuss the design before moving forward to the next step
4 |
5 | Here are some features:
6 |
7 | * Pack multiple modular or non-modular JARs into one file.
8 | * Full support for JPMS (Java Module System).
9 | * Smaller file size (via zstd, metadata compression, and constant pool sharing).
10 | * Supports declaring some requirements for Java (such as Java version) and finding a Java that meets the requirements at startup.
11 | * Supports declaring some JVM options (such as `--add-exports`, `--enable-native-access`, `-Da=b`, etc.)
12 | * Support for declaring external dependencies. Artifacts from Maven Central can be added to the classpath or module path to download on demand at startup.
13 | * Supports conditional addition of JVM options, classpath, and module paths.
14 | * Supports appending other content to the file header. For example, we can append an exe/bash launcher to the header to download Java and launch the program.
15 |
16 | My ambition is to make it the standard way of distributing Java programs.
17 |
18 | The official strong recommendation for jlink makes me feel uneasy:
19 |
20 | I want to be able to download a single program file of a few hundred KB to a few MB, rather than a compressed package of a few hundred MB;
21 | I want to avoid having dozens of JVMs and Java standard libraries on disk;
22 | I want to be able to easily get programs that work on Linux RISC-V 64 or FreeBSD.
23 |
24 | For me, the most serious problem among them is that jlink hurts the cross-platform capabilities of programs in the Java world.
25 |
26 | Think about it, in a world dominated by jlink and jpackage, who cares about users of niche platforms like FreeBSD, AIX, and Linux RISC-V 64?
27 |
28 | Although jlink can package program for other platforms, it is different from Golang,
29 | which can easily generate small executable files for different platforms.
30 | The jlink needs to download a JDK for each target platform,
31 | and the packaged program ranges from tens of megabytes to hundreds of megabytes.
32 |
33 | For each additional target platform,
34 | an additional 200MB JDK must be downloaded to the packaging device,
35 | the packaging time increases by tens of seconds to several minutes,
36 | and finally an additional file of tens to hundreds of MB must be distributed.
37 | This reduces the incentive for developers to provide distribution to more platforms.
38 |
39 | Another thing to consider is, where do we download these JDKs from?
40 | Most developers choose a vendor they trust (such as Eclipse Adoptium, BellSoft, and Azul)
41 | and download all JDKs from him.
42 | This means the compatibility of the programs they distribute often depends on this vendor.
43 |
44 | The platforms supported by these vendors cover most of the common platforms,
45 | but there are some niche platforms that are not taken care of.
46 | For example, platforms like FreeBSD, Alpine Linux, and Linux LoongArch 64 are rarely considered by JDK vendors,
47 | and Java on these platforms is often provided by the package manager.
48 | Therefore, these platforms are rarely considered by developers who use jlink to package programs.
49 |
50 | Due to these dissatisfactions, I developed the japp project.
51 |
52 | If you have the same ambition as me, please give me a hand.
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2024 Glavo
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | [versions]
16 | zstd-jni = "1.5.6-8"
17 |
18 | [libraries]
19 | zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd-jni" }
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Glavo/japp/f288a5458a50d22714b294fb3a893ac7bf1d1dff/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/native/.gitignore:
--------------------------------------------------------------------------------
1 | /target
--------------------------------------------------------------------------------
/native/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "japp"
7 | version = "0.1.0"
8 |
--------------------------------------------------------------------------------
/native/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "japp"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 |
--------------------------------------------------------------------------------
/native/src/launcher.rs:
--------------------------------------------------------------------------------
1 | use std::process::ExitCode;
2 |
3 | pub fn run_japp(args: Vec) -> ExitCode {
4 | let mut options = Vec::new();
5 | let mut japp_file = None;
6 |
7 | let mut iter = args.iter();
8 | while let Some(arg) = iter.next() {
9 | if arg.starts_with('-') {
10 | options.push(arg);
11 | } else {
12 | japp_file = Some(arg);
13 | break;
14 | }
15 | }
16 |
17 | let japp_file = japp_file.expect("Missing japp file name");
18 |
19 | panic!("TODO: run");
20 | }
--------------------------------------------------------------------------------
/native/src/main.rs:
--------------------------------------------------------------------------------
1 | mod launcher;
2 |
3 | use std::process::ExitCode;
4 |
5 | fn main() -> ExitCode {
6 | let mut iter = std::env::args();
7 |
8 | iter.next().expect("Missing executable file name");
9 |
10 | let command = match iter.next() {
11 | Some(command) => command,
12 | None => {
13 | print_help_message();
14 | return ExitCode::SUCCESS;
15 | }
16 | };
17 |
18 | return match command.as_str() {
19 | "help" | "-help" | "--help" | "-?" => {
20 | print_help_message();
21 | ExitCode::SUCCESS
22 | }
23 | "version" | "-version" | "--version" => {
24 | panic!("TODO: version");
25 | }
26 | "run" => {
27 | launcher::run_japp(iter.collect())
28 | }
29 | _ => {
30 | eprintln!("Unsupported command: {command}");
31 | ExitCode::FAILURE
32 | }
33 | };
34 | }
35 |
36 | fn print_help_message() {
37 | println!("TODO: Help Message")
38 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "japp"
2 |
3 | include(
4 | "base",
5 | "boot",
6 |
7 | "test-case:HelloWorld",
8 | "test-case:ModulePath",
9 | )
10 |
--------------------------------------------------------------------------------
/specification.md:
--------------------------------------------------------------------------------
1 | # Specification (Draft)
2 |
3 | ## types
4 |
5 | `u1`/`u2`/`u4`/`u8`: Little-endian unsigned 1/2/4/8 byte(s) integer.
6 |
7 |
8 | Field:
9 |
10 | ```
11 | Field {
12 | u1 field_id;
13 | u1[...] field_body;
14 | }
15 | ```
16 |
17 | FieldList: A list of fields, ending with the field with field_id `0`.
18 |
19 | ## japp file
20 |
21 | File format:
22 |
23 | ```
24 | JAppFile {
25 | u4 magic_number; // 0x5050414a ("JAPP")
26 | u1[...] data_pool;
27 | BootMetadata boot_metadata;
28 | LauncherMetadata launcher_metadata;
29 | FileEnd {
30 | u4 magic_number; // 0x5050414a ("JAPP")
31 | u2 major_version;
32 | u2 minor_version;
33 | u8 flags;
34 | u8 file_size;
35 | u8 boot_metadata_offset;
36 | u8 launcher_metadata_offset;
37 | u1[24] reserved;
38 | } end;
39 | }
40 | ```
41 |
42 | ## boot
43 |
44 | [BootMetadata](boot/src/main/java/org/glavo/japp/boot/JAppBootMetadata.java):
45 |
46 | ```
47 | BootMetadata {
48 | u4 magic_number; // 0x544f4f42 ("BOOT")
49 | u4 group_count;
50 | ByteArrayPool stringsPool;
51 | ResourceGroup[group_count] groups;
52 | }
53 | ```
54 |
55 | [ByteArrayPool]():
56 |
57 | ```
58 | ByteArrayPool {
59 | u1 magic_number; // 0xf0
60 | u1 compress_method;
61 | u2 resvered;
62 | u4 count;
63 | u4 uncompressed_bytes_size;
64 | u4 compressed_bytes_size;
65 | u2[count] sizes;
66 | u1[compressed_length] compressed_bytes;
67 | }
68 | ```
69 |
70 | [ResourceGroup](boot/src/main/java/org/glavo/japp/boot/JAppResourceGroup.java):
71 |
72 | ```
73 | ResourceGroup {
74 | u1 magic_number; // 0xeb
75 | u1 compress_method;
76 | u2 reserved;
77 |
78 | u4 uncompressed_size;
79 | u4 compressed_size;
80 | u4 resources_count;
81 | u8 checksum;
82 |
83 | u1[compressed_size] compressed_resources;
84 | }
85 | ```
86 |
87 | [Resource](boot/src/main/java/org/glavo/japp/boot/JAppResource.java):
88 |
89 | ```
90 | Resource {
91 | u1 magic_number; // 0x1b
92 | u1 compress_method;
93 | u2 path_length;
94 | u4 reserved;
95 | u8 uncompressed_size;
96 | u8 compressed_size;
97 | u8 content_offset;
98 | u1[path_length] path; // UTF-8
99 |
100 | ResourceFields optional_fields;
101 | }
102 | ```
103 |
104 | ## launcher
105 |
106 | [LauncherMetadata](src/main/java/org/glavo/japp/launcher/JAppLauncherMetadata.java):
107 |
108 | ```
109 | LauncherMetadata {
110 | ConfigGroup root_group;
111 | }
112 | ```
113 |
114 | [ConfigGroup](src/main/java/org/glavo/japp/launcher/JAppConfigGroup.java):
115 |
116 | ```
117 | ConfigGroup {
118 | u4 magic_number; // 0x00505247 ("GRP\0")
119 | ConfigGroupFields fields;
120 | }
121 | ```
122 |
123 | [ResourceGroupReference](src/main/java/org/glavo/japp/JAppResourceGroupReference.java):
124 |
125 | ```
126 | ResourceGroupReference {
127 | u1 id;
128 | String name;
129 | union {
130 | LocalResourceGroupReference local_reference;
131 | MavenResourceGroupReference maven_reference;
132 | } reference;
133 | }
134 |
135 | LocalResourceGroupReference {
136 |
137 | u4 index;
138 | u4 multi_count;
139 | {
140 | u4 multi_version;
141 | u4 multi_index;
142 | }[multi_count] multi_index_pairs;
143 | }
144 |
145 | MavenResourceGroupReference {
146 | String repository;
147 | String group;
148 | String artifact;
149 | String version;
150 | String classifier;
151 | }
152 | ```
153 |
154 | ## share
155 |
156 | Pass data from launcher to boot launcher: `-Dorg.glavo.japp.args=`
157 |
158 | ```
159 | BootArgs {
160 | String japp_file;
161 | u8 base_offset;
162 | u8 boot_metadata_offset;
163 | u8 boot_metadata_size;
164 | BootArgFields fields;
165 | }
166 | ```
167 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/Main.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp;
17 |
18 | import org.glavo.japp.launcher.Launcher;
19 | import org.glavo.japp.packer.JAppPacker;
20 | import org.glavo.japp.platform.JavaRuntime;
21 |
22 | import java.io.PrintStream;
23 | import java.util.Arrays;
24 |
25 | public final class Main {
26 | private static void printHelpMessage(PrintStream out) {
27 | out.println("Usage: japp [options]");
28 | out.println("Supported mode:");
29 | out.println(" japp create");
30 | out.println(" japp run");
31 | out.println(" japp list-java");
32 | }
33 |
34 | public static void main(String[] args) throws Throwable {
35 | if (args.length == 0) {
36 | printHelpMessage(System.out);
37 | return;
38 | }
39 |
40 | String[] commandArgs = Arrays.copyOfRange(args, 1, args.length);
41 |
42 | switch (args[0]) {
43 | case "help":
44 | case "-help":
45 | case "--help":
46 | printHelpMessage(System.out);
47 | return;
48 | case "create":
49 | JAppPacker.main(commandArgs);
50 | break;
51 | case "run":
52 | Launcher.main(commandArgs);
53 | break;
54 | case "list-java":
55 | JavaRuntime.main(commandArgs);
56 | break;
57 | default:
58 | System.err.println("Unsupported mode: " + args[0]);
59 | printHelpMessage(System.err);
60 | System.exit(1);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/condition/AndCondition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.condition;
17 |
18 | import org.glavo.japp.platform.JAppRuntimeContext;
19 |
20 | import java.util.Iterator;
21 | import java.util.List;
22 |
23 | public final class AndCondition implements Condition {
24 | private final List conditions;
25 |
26 | public AndCondition(List conditions) {
27 | assert conditions.size() >= 2;
28 | this.conditions = conditions;
29 | }
30 |
31 | @Override
32 | public boolean test(JAppRuntimeContext context) {
33 | for (Condition condition : conditions) {
34 | if (!condition.test(context)) {
35 | return false;
36 | }
37 | }
38 |
39 | return true;
40 | }
41 |
42 | @Override
43 | public String toString() {
44 | StringBuilder res = new StringBuilder();
45 | Iterator it = conditions.iterator();
46 | res.append("and(");
47 | res.append(it.next());
48 | while (it.hasNext()) {
49 | res.append(", ").append(it.next());
50 | }
51 | res.append(')');
52 | return res.toString();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/condition/Condition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | */
17 | package org.glavo.japp.condition;
18 |
19 | import org.glavo.japp.platform.JAppRuntimeContext;
20 |
21 | import java.util.function.Predicate;
22 |
23 | @FunctionalInterface
24 | public interface Condition extends Predicate {
25 | boolean test(JAppRuntimeContext context);
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/condition/JavaCondition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.condition;
17 |
18 | import org.glavo.japp.platform.JAppRuntimeContext;
19 |
20 | import java.util.Map;
21 | import java.util.StringJoiner;
22 |
23 | public final class JavaCondition implements Condition {
24 |
25 | public static JavaCondition fromMap(Map options) {
26 | String version = options.remove("version");
27 | String os = options.remove("os");
28 | String arch = options.remove("arch");
29 | String libc = options.remove("libc");
30 |
31 | if (!options.isEmpty()) {
32 | throw new IllegalArgumentException("Unknown options: " + options.keySet());
33 | }
34 |
35 | return new JavaCondition(
36 | version == null ? null : Integer.parseInt(version),
37 | MatchList.of(os), MatchList.of(arch), MatchList.of(libc)
38 | );
39 | }
40 |
41 | private final Integer version;
42 | private final MatchList os;
43 | private final MatchList arch;
44 | private final MatchList libc;
45 |
46 | private JavaCondition(Integer version, MatchList os, MatchList arch, MatchList libc) {
47 | this.version = version;
48 | this.os = os;
49 | this.arch = arch;
50 | this.libc = libc;
51 | }
52 |
53 | @Override
54 | @SuppressWarnings("deprecation")
55 | public boolean test(JAppRuntimeContext context) {
56 | if (version != null && context.getJava().getVersion().major() < version) {
57 | return false;
58 | }
59 |
60 | if (os != null && !os.test(context.getJava().getOperatingSystem().getCheckedName())) {
61 | return false;
62 | }
63 |
64 | if (arch != null && !arch.test(context.getJava().getArchitecture().getCheckedName())) {
65 | return false;
66 | }
67 |
68 | if (libc != null && !libc.test(context.getJava().getLibC().toString())) {
69 | return false;
70 | }
71 |
72 | return true;
73 | }
74 |
75 | @Override
76 | public String toString() {
77 | StringJoiner joiner = new StringJoiner(", ", "java(", ")");
78 | if (version != null) {
79 | joiner.add("version=" + version);
80 | }
81 | if (os != null) {
82 | joiner.add("os=" + os);
83 | }
84 | if (arch != null) {
85 | joiner.add("arch=" + arch);
86 | }
87 | if (libc != null) {
88 | joiner.add("libc=" + libc);
89 | }
90 | return joiner.toString();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/condition/MatchList.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.condition;
17 |
18 | import java.util.Arrays;
19 | import java.util.List;
20 | import java.util.function.Predicate;
21 |
22 | public final class MatchList implements Predicate {
23 |
24 | public static MatchList of(String list) {
25 | if (list == null) {
26 | return null;
27 | }
28 |
29 | boolean negative;
30 | if (list.startsWith("!")) {
31 | list = list.substring(1);
32 | negative = true;
33 | } else {
34 | negative = false;
35 | }
36 |
37 | return new MatchList(negative, Arrays.asList(list.split("\\|")));
38 | }
39 |
40 | private final boolean negative;
41 | private final List values;
42 |
43 | public MatchList(boolean negative, List values) {
44 | this.negative = negative;
45 | this.values = values;
46 | }
47 |
48 | @Override
49 | public boolean test(String s) {
50 | return negative != values.contains(s);
51 | }
52 |
53 | @Override
54 | public String toString() {
55 | return (negative ? "!" : "") + values;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/condition/NotCondition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.condition;
17 |
18 | import org.glavo.japp.platform.JAppRuntimeContext;
19 |
20 | public final class NotCondition implements Condition {
21 | private final Condition original;
22 |
23 | public NotCondition(Condition original) {
24 | this.original = original;
25 | }
26 |
27 | @Override
28 | public boolean test(JAppRuntimeContext context) {
29 | return !original.test(context);
30 | }
31 |
32 | @Override
33 | public String toString() {
34 | return "not(" + original + ')';
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/condition/OrCondition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.condition;
17 |
18 | import org.glavo.japp.platform.JAppRuntimeContext;
19 |
20 | import java.util.Iterator;
21 | import java.util.List;
22 |
23 | public final class OrCondition implements Condition {
24 | private final List conditions;
25 |
26 | public OrCondition(List conditions) {
27 | assert conditions.size() >= 2;
28 | this.conditions = conditions;
29 | }
30 |
31 | @Override
32 | public boolean test(JAppRuntimeContext context) {
33 | for (Condition condition : conditions) {
34 | if (condition.test(context)) {
35 | return true;
36 | }
37 | }
38 |
39 | return false;
40 | }
41 |
42 | @Override
43 | public String toString() {
44 | StringBuilder res = new StringBuilder();
45 | Iterator it = conditions.iterator();
46 | res.append("or(");
47 | res.append(it.next());
48 | while (it.hasNext()) {
49 | res.append(", ").append(it.next());
50 | }
51 | res.append(')');
52 | return res.toString();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/launcher/EmbeddedLauncher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.launcher;
17 |
18 | import org.glavo.japp.Main;
19 |
20 | import java.nio.file.Paths;
21 | import java.util.Arrays;
22 | import java.util.Collections;
23 |
24 | public final class EmbeddedLauncher {
25 | public static void main(String[] args) throws Throwable {
26 | Launcher.run(Paths.get(Main.class.getProtectionDomain().getCodeSource().getLocation().toURI()), Collections.emptyList(), Arrays.asList(args));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/maven/MavenResolver.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.maven;
17 |
18 | import java.nio.file.Path;
19 | import java.nio.file.Paths;
20 | import java.util.HashMap;
21 | import java.util.Map;
22 |
23 | public final class MavenResolver {
24 |
25 | public static final MavenRepository CENTRAL = new MavenRepository.Remote("central", "https://repo1.maven.org/maven2");
26 | public static final MavenRepository LOCAL = new MavenRepository.Local("local", Paths.get(System.getProperty("user.home"), ".m2", ".repository"));
27 |
28 | private static final Map repos = new HashMap<>();
29 |
30 | static {
31 | repos.put(CENTRAL.getName(), CENTRAL);
32 | repos.put(LOCAL.getName(), LOCAL);
33 | }
34 |
35 | public static Path resolve(String repoName, String group, String artifact, String version, String classifier) throws Throwable {
36 | MavenRepository repo;
37 | if (repoName == null) {
38 | repo = CENTRAL;
39 | } else {
40 | repo = repos.get(repoName);
41 | if (repo == null) {
42 | throw new IllegalArgumentException("Unknown repo: " + repoName);
43 | }
44 | }
45 |
46 | return repo.resolve(group, artifact, version, classifier);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/JAppResourceInfo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer;
17 |
18 | import org.glavo.japp.CompressionMethod;
19 |
20 | import java.nio.file.attribute.FileTime;
21 |
22 | public final class JAppResourceInfo {
23 | final String name;
24 |
25 | FileTime creationTime;
26 | FileTime lastModifiedTime;
27 | FileTime lastAccessTime;
28 |
29 | boolean hasWritten = false;
30 | long offset;
31 | long size;
32 | CompressionMethod method;
33 | long compressedSize;
34 |
35 | Long checksum;
36 |
37 | public JAppResourceInfo(String name) {
38 | this.name = name;
39 | }
40 |
41 | public void setCreationTime(FileTime creationTime) {
42 | this.creationTime = creationTime;
43 | }
44 |
45 | public void setLastModifiedTime(FileTime lastModifiedTime) {
46 | this.lastModifiedTime = lastModifiedTime;
47 | }
48 |
49 | public void setLastAccessTime(FileTime lastAccessTime) {
50 | this.lastAccessTime = lastAccessTime;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/JAppResourcesWriter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer;
17 |
18 | import org.glavo.japp.launcher.JAppResourceGroupReference;
19 | import org.glavo.japp.packer.compressor.CompressResult;
20 | import org.glavo.japp.util.XxHash64;
21 |
22 | import java.io.IOException;
23 | import java.util.LinkedHashMap;
24 | import java.util.List;
25 | import java.util.Map;
26 | import java.util.TreeMap;
27 |
28 | public final class JAppResourcesWriter implements AutoCloseable {
29 | private final JAppWriter writer;
30 | private final String name;
31 | private final List referenceList;
32 |
33 | private final Map resources = new LinkedHashMap<>();
34 | private final Map> multiReleaseResources = new TreeMap<>();
35 |
36 | JAppResourcesWriter(JAppWriter writer, String name, List referenceList) {
37 | this.writer = writer;
38 | this.name = name;
39 | this.referenceList = referenceList;
40 | }
41 |
42 | public void writeResource(JAppResourceInfo resource, byte[] body) throws IOException {
43 | writeResource(-1, resource, body);
44 | }
45 |
46 | public void writeResource(int release, JAppResourceInfo resource, byte[] body) throws IOException {
47 | if (resource.hasWritten) {
48 | throw new AssertionError("Resource " + resource.name + " has been written");
49 | }
50 |
51 | resource.hasWritten = true;
52 |
53 | Map resources;
54 | if (release == -1) {
55 | resources = this.resources;
56 | } else {
57 | resources = this.multiReleaseResources.computeIfAbsent(release, r -> new LinkedHashMap<>());
58 | }
59 |
60 | resources.put(resource.name, resource);
61 | resource.offset = writer.getCurrentOffset();
62 | resource.size = body.length;
63 | resource.checksum = XxHash64.hash(body);
64 |
65 | CompressResult result = writer.compressor.compress(writer, body, resource.name);
66 | resource.method = result.getMethod();
67 | resource.compressedSize = result.getLength();
68 |
69 | writer.getOutput().writeBytes(result.getCompressedData(), result.getOffset(), result.getLength());
70 | }
71 |
72 | private int addGroup(Map group) {
73 | int index = writer.groups.size();
74 | writer.groups.add(group);
75 | return index;
76 | }
77 |
78 | public void close() {
79 | int baseIndex = addGroup(resources);
80 | TreeMap multiIndexes;
81 | if (!multiReleaseResources.isEmpty()) {
82 | multiIndexes = new TreeMap<>();
83 | multiReleaseResources.forEach((i, g) -> multiIndexes.put(i, addGroup(g)));
84 | } else {
85 | multiIndexes = null;
86 | }
87 | referenceList.add(new JAppResourceGroupReference.Local(name, baseIndex, multiIndexes));
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/ModuleInfoReader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer;
17 |
18 | import org.glavo.japp.packer.compressor.classfile.ClassFileReader;
19 |
20 | import java.io.IOException;
21 | import java.io.InputStream;
22 | import java.lang.module.ModuleDescriptor;
23 | import java.nio.ByteBuffer;
24 | import java.util.regex.Matcher;
25 | import java.util.regex.Pattern;
26 |
27 | public final class ModuleInfoReader {
28 | private static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
29 | private static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
30 | private static final Pattern REPEATING_DOTS = Pattern.compile("(\\.)(\\1)+");
31 | private static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
32 |
33 | public static String deriveAutomaticModuleName(String jarFileName) {
34 | if (!jarFileName.endsWith(".jar")) {
35 | throw new IllegalArgumentException(jarFileName);
36 | }
37 |
38 | final int end = jarFileName.length() - ".jar".length();
39 |
40 | int start = 0;
41 | for (; start < end; start++) {
42 | if (jarFileName.charAt(start) != '.') {
43 | break;
44 | }
45 | }
46 |
47 | if (start == end) {
48 | throw new IllegalArgumentException(jarFileName);
49 | }
50 |
51 | String name = jarFileName.substring(start, end);
52 |
53 | // find first occurrence of -${NUMBER}. or -${NUMBER}$
54 | Matcher matcher = DASH_VERSION.matcher(name);
55 | if (matcher.find()) {
56 | name = name.substring(0, matcher.start());
57 | }
58 |
59 | name = NON_ALPHANUM.matcher(name).replaceAll(".");
60 | name = REPEATING_DOTS.matcher(name).replaceAll(".");
61 |
62 | // drop trailing dots
63 | int len = name.length();
64 | if (len > 0 && name.charAt(len - 1) == '.') {
65 | name = TRAILING_DOTS.matcher(name).replaceAll("");
66 | }
67 |
68 | return name;
69 | }
70 |
71 | public static String readModuleName(InputStream moduleInfo) throws IOException {
72 | String moduleName = new ClassFileReader(ByteBuffer.wrap(moduleInfo.readAllBytes())).getModuleName();
73 | if (moduleName == null) {
74 | throw new IOException();
75 | }
76 | return moduleName;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/compressor/CompressContext.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.compressor;
17 |
18 | import com.github.luben.zstd.ZstdCompressCtx;
19 | import org.glavo.japp.packer.compressor.classfile.ByteArrayPoolBuilder;
20 |
21 | public interface CompressContext {
22 | ByteArrayPoolBuilder getPool();
23 |
24 | default ZstdCompressCtx getZstdCompressCtx() {
25 | return new ZstdCompressCtx();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/compressor/CompressResult.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.compressor;
17 |
18 | import org.glavo.japp.CompressionMethod;
19 |
20 | import java.nio.ByteBuffer;
21 | import java.util.Arrays;
22 |
23 | public final class CompressResult {
24 | private final CompressionMethod method;
25 | private final byte[] compressedData;
26 |
27 | private final int offset;
28 | private final int length;
29 |
30 | public CompressResult(byte[] compressedData) {
31 | this(CompressionMethod.NONE, compressedData, 0, compressedData.length);
32 | }
33 |
34 | public CompressResult(CompressionMethod method, byte[] compressedData) {
35 | this(method, compressedData, 0, compressedData.length);
36 | }
37 |
38 | public CompressResult(CompressionMethod method, byte[] compressedData, int offset, int length) {
39 | this.method = method;
40 | this.compressedData = compressedData;
41 | this.offset = offset;
42 | this.length = length;
43 | }
44 |
45 | public CompressionMethod getMethod() {
46 | return method;
47 | }
48 |
49 | public ByteBuffer getCompressed() {
50 | return ByteBuffer.wrap(compressedData, offset, length);
51 | }
52 |
53 | public byte[] getCompressedData() {
54 | return compressedData;
55 | }
56 |
57 | public int getOffset() {
58 | return offset;
59 | }
60 |
61 | public int getLength() {
62 | return length;
63 | }
64 |
65 | @Override
66 | public String toString() {
67 | return "CompressResult{" +
68 | "method=" + method +
69 | ", compressedData=" + Arrays.toString(compressedData) +
70 | ", offset=" + offset +
71 | ", length=" + length +
72 | '}';
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/compressor/Compressor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.compressor;
17 |
18 | import java.io.IOException;
19 |
20 | @FunctionalInterface
21 | public interface Compressor {
22 |
23 | CompressResult compress(CompressContext context, byte[] source) throws IOException;
24 |
25 | default CompressResult compress(CompressContext context, byte[] source, String filePath) throws IOException {
26 | return compress(context, source);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/compressor/Compressors.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.compressor;
17 |
18 | import org.glavo.japp.CompressionMethod;
19 | import org.glavo.japp.util.ZstdUtils;
20 | import org.glavo.japp.packer.compressor.classfile.ClassFileCompressor;
21 |
22 | public final class Compressors {
23 |
24 | public static final Compressor DEFAULT = new DefaultCompressor();
25 |
26 | public static final Compressor CLASSFILE = new ClassFileCompressor();
27 |
28 | public static final Compressor ZSTD = (context, source) -> {
29 | byte[] res = new byte[ZstdUtils.maxCompressedLength(source.length)];
30 | long n = context.getZstdCompressCtx().compressByteArray(res, 0, res.length, source, 0, source.length);
31 | return new CompressResult(CompressionMethod.ZSTD, res, 0, (int) n);
32 | };
33 |
34 | private Compressors() {
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/compressor/DefaultCompressor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.compressor;
17 |
18 | import org.glavo.japp.CompressionMethod;
19 |
20 | import java.io.IOException;
21 | import java.util.HashMap;
22 | import java.util.Map;
23 |
24 | final class DefaultCompressor implements Compressor {
25 |
26 | private final Map map = new HashMap<>();
27 | private final CompressionMethod defaultMethod = CompressionMethod.ZSTD;
28 |
29 | public DefaultCompressor() {
30 | map.put("class", CompressionMethod.CLASSFILE);
31 |
32 | for (String ext : new String[]{
33 | "png", "apng", "jpg", "jpeg", "webp", "heic", "heif", "avif",
34 | "aac", "flac", "mp3",
35 | "mp4", "mkv", "webm",
36 | "gz", "tgz", "xz", "br", "zst", "bz2", "tbz2"
37 | }) {
38 | map.put(ext, CompressionMethod.NONE);
39 | }
40 | }
41 |
42 | @Override
43 | public CompressResult compress(CompressContext context, byte[] source) throws IOException {
44 | return compress(context, source, null);
45 | }
46 |
47 | @Override
48 | public CompressResult compress(CompressContext context, byte[] source, String filePath) throws IOException {
49 | if (source.length <= 16) {
50 | return new CompressResult(source);
51 | }
52 |
53 | String ext;
54 | if (filePath != null) {
55 | int sep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
56 | int dot = filePath.lastIndexOf('.');
57 | ext = dot > sep ? filePath.substring(dot + 1) : null;
58 | } else {
59 | ext = null;
60 | }
61 |
62 | CompressionMethod method = map.getOrDefault(ext, defaultMethod);
63 | CompressResult result;
64 | switch (method) {
65 | case NONE:
66 | result = new CompressResult(source);
67 | break;
68 | case CLASSFILE:
69 | try {
70 | result = Compressors.CLASSFILE.compress(context, source);
71 | } catch (Throwable e) {
72 | // Malformed class file
73 | result = Compressors.ZSTD.compress(context, source);
74 | }
75 | break;
76 | case ZSTD:
77 | result = Compressors.ZSTD.compress(context, source);
78 | break;
79 | default:
80 | throw new AssertionError("Unimplemented compression method: " + method);
81 | }
82 |
83 | return result.getLength() < source.length ? result : new CompressResult(source);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/compressor/classfile/ByteArrayPoolBuilder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.compressor.classfile;
17 |
18 | import com.github.luben.zstd.Zstd;
19 | import org.glavo.japp.CompressionMethod;
20 | import org.glavo.japp.boot.decompressor.classfile.ByteArrayPool;
21 | import org.glavo.japp.boot.decompressor.zstd.ZstdFrameDecompressor;
22 | import org.glavo.japp.io.LittleEndianDataOutput;
23 | import org.glavo.japp.util.ZstdUtils;
24 | import org.glavo.japp.io.ByteBufferOutputStream;
25 |
26 | import java.io.IOException;
27 | import java.nio.ByteBuffer;
28 | import java.nio.ByteOrder;
29 | import java.util.Arrays;
30 | import java.util.HashMap;
31 |
32 | public final class ByteArrayPoolBuilder {
33 | private static final class ByteArrayWrapper {
34 | final byte[] bytes;
35 | final int hash;
36 |
37 | private ByteArrayWrapper(byte[] bytes) {
38 | this.bytes = bytes;
39 | this.hash = Arrays.hashCode(bytes);
40 | }
41 |
42 | @Override
43 | public int hashCode() {
44 | return hash;
45 | }
46 |
47 | @Override
48 | public boolean equals(Object obj) {
49 | if (!(obj instanceof ByteArrayWrapper)) {
50 | return false;
51 | }
52 |
53 | ByteArrayWrapper other = (ByteArrayWrapper) obj;
54 | return Arrays.equals(this.bytes, other.bytes);
55 | }
56 | }
57 |
58 | private final HashMap map = new HashMap<>();
59 | private ByteBuffer bytes = ByteBuffer.allocate(8192).order(ByteOrder.LITTLE_ENDIAN);
60 | private ByteBuffer sizes = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN);
61 |
62 | private void growIfNeed(int s) {
63 | if (bytes.remaining() < s) {
64 | int position = bytes.position();
65 | int nextLen = Math.max(bytes.limit() * 2, position + s);
66 | bytes = ByteBuffer.wrap(Arrays.copyOf(bytes.array(), nextLen)).position(position);
67 | }
68 |
69 | if (!sizes.hasRemaining()) {
70 | int position = sizes.position();
71 | sizes = ByteBuffer.wrap(Arrays.copyOf(sizes.array(), position * 2))
72 | .position(position)
73 | .order(ByteOrder.LITTLE_ENDIAN);
74 | }
75 | }
76 |
77 | public int add(byte[] bytes) {
78 | assert bytes.length <= 0xffff;
79 |
80 | ByteArrayWrapper wrapper = new ByteArrayWrapper(bytes);
81 |
82 | Integer index = map.get(wrapper);
83 | if (index != null) {
84 | return index;
85 | }
86 |
87 | index = map.size();
88 | map.put(wrapper, index);
89 |
90 | growIfNeed(bytes.length);
91 | this.sizes.putShort((short) bytes.length);
92 | this.bytes.put(bytes);
93 |
94 | return index;
95 | }
96 |
97 | public void writeTo(LittleEndianDataOutput output) throws IOException {
98 | int count = map.size();
99 | int uncompressedBytesSize = bytes.position();
100 |
101 | CompressionMethod compressionMethod;
102 | byte[] compressed = new byte[ZstdUtils.maxCompressedLength(uncompressedBytesSize)];
103 | long compressedBytesSize = Zstd.compressByteArray(compressed, 0, compressed.length, bytes.array(), 0, uncompressedBytesSize, 8);
104 | if (compressedBytesSize < uncompressedBytesSize) {
105 | compressionMethod = CompressionMethod.ZSTD;
106 | } else {
107 | compressionMethod = CompressionMethod.NONE;
108 | compressed = bytes.array();
109 | compressedBytesSize = uncompressedBytesSize;
110 | }
111 |
112 | output.writeByte(ByteArrayPool.MAGIC_NUMBER);
113 | output.writeByte(compressionMethod.id());
114 | output.writeShort((short) 0);
115 | output.writeInt(count);
116 | output.writeInt(uncompressedBytesSize);
117 | output.writeInt((int) compressedBytesSize);
118 | output.writeBytes(sizes.array(), 0, sizes.position());
119 | output.writeBytes(compressed, 0, (int) compressedBytesSize);
120 | }
121 |
122 | public ByteArrayPool toPool() throws IOException {
123 | ByteBufferOutputStream output = new ByteBufferOutputStream();
124 | writeTo(output);
125 | return ByteArrayPool.readFrom(output.getByteBuffer().flip(), new ZstdFrameDecompressor());
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/processor/ClassPathProcessor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.processor;
17 |
18 | import org.glavo.japp.packer.JAppWriter;
19 |
20 | import java.util.Map;
21 |
22 | public abstract class ClassPathProcessor {
23 | private static ClassPathProcessor getProcessor(String type) {
24 | if (type == null) {
25 | return LocalClassPathProcessor.INSTANCE;
26 | }
27 | switch (type) {
28 | case "local":
29 | return LocalClassPathProcessor.INSTANCE;
30 | case "maven":
31 | return new MavenClassPathProcessor();
32 | default:
33 | throw new IllegalArgumentException("Unknown type: " + type);
34 | }
35 | }
36 |
37 | public static void process(JAppWriter writer, String pathList, boolean isModulePath) throws Throwable {
38 | if (pathList == null || pathList.isEmpty()) {
39 | return;
40 | }
41 |
42 | PathListParser parser = new PathListParser(pathList);
43 | while (parser.scanNext()) {
44 | Map options = parser.options;
45 | String path = parser.path;
46 |
47 | ClassPathProcessor processor = getProcessor(options.remove("type"));
48 | processor.process(writer, path, isModulePath, options);
49 | }
50 | }
51 |
52 | public abstract void process(JAppWriter packer, String path, boolean isModulePath, Map options) throws Throwable;
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/processor/MavenClassPathProcessor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.processor;
17 |
18 | import org.glavo.japp.launcher.JAppResourceGroupReference;
19 | import org.glavo.japp.maven.MavenResolver;
20 | import org.glavo.japp.packer.JAppWriter;
21 | import org.glavo.japp.packer.ModuleInfoReader;
22 |
23 | import java.lang.module.ModuleFinder;
24 | import java.lang.module.ModuleReference;
25 | import java.nio.file.Path;
26 | import java.util.Map;
27 | import java.util.Set;
28 | import java.util.regex.Matcher;
29 | import java.util.regex.Pattern;
30 |
31 | public final class MavenClassPathProcessor extends ClassPathProcessor {
32 |
33 | private final Pattern pattern = Pattern.compile("(?[^/]+)/(?[^/]+)/(?[^/]+)(/(?[^/]+))?");
34 |
35 | @Override
36 | public void process(JAppWriter packer, String path, boolean isModulePath, Map options) throws Throwable {
37 | boolean bundle = !"false".equals(options.remove("bundle"));
38 | String repo = options.remove("repository");
39 | boolean verify = !"false".equals(options.remove("verify")); // TODO
40 |
41 | if (!options.isEmpty()) {
42 | throw new IllegalArgumentException("Unrecognized options: " + options.keySet());
43 | }
44 |
45 | Matcher matcher = pattern.matcher(path);
46 | if (!matcher.matches()) {
47 | throw new IllegalArgumentException("Invalid path: " + path);
48 | }
49 |
50 | String group = matcher.group("group");
51 | String artifact = matcher.group("artifact");
52 | String version = matcher.group("version");
53 | String classifier = matcher.group("classifier");
54 |
55 | Path file = MavenResolver.resolve(repo, group, artifact, version, classifier);
56 |
57 | if (bundle) {
58 | LocalClassPathProcessor.addJar(packer, file, isModulePath);
59 | } else {
60 | String name;
61 | if (isModulePath) {
62 | ModuleFinder finder = ModuleFinder.of(file); // TODO: need opt
63 | Set all = finder.findAll();
64 | assert all.size() == 1;
65 | name = all.iterator().next().descriptor().name();
66 | } else {
67 | name = file.getFileName().toString();
68 | }
69 |
70 | packer.addReference(
71 | new JAppResourceGroupReference.Maven(name, repo, group, artifact, version, classifier),
72 | isModulePath
73 | );
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/packer/processor/PathListParser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.processor;
17 |
18 | import java.io.File;
19 | import java.util.LinkedHashMap;
20 | import java.util.Map;
21 |
22 | final class PathListParser {
23 | static final char pathSeparatorChar = File.pathSeparatorChar;
24 |
25 | private final String list;
26 | private int offset = 0;
27 |
28 | Map options;
29 | String path;
30 |
31 | PathListParser(String list) {
32 | this.list = list;
33 | }
34 |
35 | private void ensureNotAtEnd() {
36 | if (offset >= list.length()) {
37 | throw new IllegalArgumentException("Unexpected end of input");
38 | }
39 | }
40 |
41 | private void skipWhitespace() {
42 | while (offset < list.length() && list.charAt(offset) == ' ') {
43 | offset++;
44 | }
45 | }
46 |
47 | private boolean isLiteralChar(char ch) {
48 | switch (ch) {
49 | case '@':
50 | case '#':
51 | case '%':
52 | case '+':
53 | case '-':
54 | case '*':
55 | case '/':
56 | case '!':
57 | return true;
58 | default:
59 | return Character.isJavaIdentifierPart(ch);
60 | }
61 | }
62 |
63 | private String nextLiteral() {
64 | if (offset >= list.length()) {
65 | return "";
66 | }
67 |
68 | String res;
69 | char ch = list.charAt(offset);
70 | if (ch == '\'' || ch == '"') {
71 | int end = list.indexOf(ch, offset + 1);
72 | if (end < 0) {
73 | throw new IllegalArgumentException();
74 | }
75 |
76 | res = list.substring(offset + 1, end);
77 | offset = end + 1;
78 | } else if (isLiteralChar(ch)) {
79 | int end = offset + 1;
80 | while (end < list.length() && isLiteralChar(list.charAt(end))) {
81 | end++;
82 | }
83 | res = list.substring(offset, end);
84 | offset = end;
85 | } else {
86 | throw new IllegalArgumentException();
87 | }
88 |
89 | return res;
90 | }
91 |
92 | private void readPair() {
93 | String name = nextLiteral();
94 | if (name.isEmpty()) {
95 | throw new IllegalArgumentException("Option name cannot be empty");
96 | }
97 |
98 | skipWhitespace();
99 | ensureNotAtEnd();
100 |
101 | if (list.charAt(offset++) != '=') {
102 | throw new IllegalArgumentException();
103 | }
104 |
105 | skipWhitespace();
106 | String value = nextLiteral();
107 |
108 | if (options.put(name, value) != null) {
109 | throw new IllegalArgumentException("Duplicate option: " + name);
110 | }
111 | }
112 |
113 | boolean scanNext() {
114 | if (offset >= list.length()) {
115 | return false;
116 | }
117 |
118 | char ch = 0;
119 | while (offset < list.length() && (ch = list.charAt(offset)) == pathSeparatorChar) {
120 | offset++;
121 | }
122 |
123 | if (offset >= list.length()) {
124 | return false;
125 | }
126 |
127 | options = new LinkedHashMap<>();
128 | if (ch == '[') {
129 | offset++;
130 | skipWhitespace();
131 | ensureNotAtEnd();
132 |
133 | boolean isFirst = true;
134 | while ((ch = list.charAt(offset)) != ']') {
135 | if (isFirst) {
136 | isFirst = false;
137 | } else {
138 | if (ch != ',') {
139 | throw new IllegalArgumentException();
140 | }
141 | offset++;
142 | skipWhitespace();
143 | ensureNotAtEnd();
144 | }
145 |
146 | readPair();
147 | skipWhitespace();
148 | ensureNotAtEnd();
149 | }
150 |
151 | offset++; // skip ']'
152 | }
153 |
154 | int end = list.indexOf(pathSeparatorChar, offset);
155 | if (end < 0) {
156 | path = list.substring(offset);
157 | offset = list.length();
158 | } else {
159 | path = list.substring(offset, end);
160 | offset = end + 1;
161 | }
162 |
163 | return true;
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/platform/Architecture.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.platform;
17 |
18 | import java.util.Locale;
19 |
20 | public enum Architecture {
21 | X86(false, "x86"),
22 | X86_64(true, "x86-64"),
23 | IA64(true, "IA-64"),
24 | SPARC(false, "SPARC"),
25 | SPARCV9(true, "SPARC V9"),
26 | ARM(false, "ARM"),
27 | AARCH64(true, "AArch64"),
28 | MIPS(false, "MIPS (Big-Endian)"),
29 | MIPS64(true, "MIPS64 (Big-Endian)"),
30 | MIPSEL(false, "MIPS (Little-Endian)"),
31 | MIPS64EL(true, "MIPS64 (Little-Endian)"),
32 | PPC(false, "PowerPC (Big-Endian)"),
33 | PPC64(true, "PowerPC-64 (Big-Endian)"),
34 | PPCLE(false, "PowerPC (Little-Endian)"),
35 | PPC64LE(true, "PowerPC-64 (Little-Endian)"),
36 | S390(false, "S390"),
37 | S390X(true, "S390x"),
38 | RISCV32(false, "RISC-V 32"),
39 | RISCV64(true, "RISC-V 64"),
40 | LOONGARCH32(false, "LoongArch32"),
41 | LOONGARCH64(true, "LoongArch64");
42 |
43 | private final String checkedName;
44 | private final String displayName;
45 | private final boolean is64Bit;
46 |
47 | Architecture(boolean is64Bit, String displayName) {
48 | this.checkedName = this.name().toLowerCase(Locale.ROOT).replace("_", "-");
49 | this.displayName = displayName;
50 | this.is64Bit = is64Bit;
51 | }
52 |
53 | public static Architecture parseArchitecture(String value) {
54 | value = value.trim().toLowerCase(Locale.ROOT);
55 |
56 | switch (value) {
57 | case "x8664":
58 | case "x86-64":
59 | case "x86_64":
60 | case "amd64":
61 | case "ia32e":
62 | case "em64t":
63 | case "x64":
64 | return X86_64;
65 | case "x8632":
66 | case "x86-32":
67 | case "x86_32":
68 | case "x86":
69 | case "i86pc":
70 | case "i386":
71 | case "i486":
72 | case "i586":
73 | case "i686":
74 | case "ia32":
75 | case "x32":
76 | return X86;
77 | case "arm64":
78 | case "aarch64":
79 | return AARCH64;
80 | case "arm":
81 | case "arm32":
82 | return ARM;
83 | case "mips64":
84 | return MIPS64;
85 | case "mips64el":
86 | return MIPS64EL;
87 | case "mips":
88 | case "mips32":
89 | return MIPS;
90 | case "mipsel":
91 | case "mips32el":
92 | return MIPSEL;
93 | case "riscv32":
94 | return RISCV32;
95 | case "riscv64":
96 | return RISCV64;
97 | case "ia64":
98 | case "ia64w":
99 | case "itanium64":
100 | return IA64;
101 | case "sparcv9":
102 | case "sparc64":
103 | return SPARCV9;
104 | case "sparc":
105 | case "sparc32":
106 | return SPARC;
107 | case "ppc64":
108 | case "powerpc64":
109 | return PPC64;
110 | case "ppc64le":
111 | case "powerpc64le":
112 | return PPC64LE;
113 | case "ppc":
114 | case "ppc32":
115 | case "powerpc":
116 | case "powerpc32":
117 | return PPC;
118 | case "ppcle":
119 | case "ppc32le":
120 | case "powerpcle":
121 | case "powerpc32le":
122 | return PPCLE;
123 | case "s390":
124 | return S390;
125 | case "s390x":
126 | return S390X;
127 | case "loongarch32":
128 | return LOONGARCH32;
129 | case "loongarch64":
130 | return LOONGARCH64;
131 | default:
132 | if (value.startsWith("armv7")) {
133 | return ARM;
134 | }
135 | if (value.startsWith("armv8") || value.startsWith("armv9")) {
136 | return AARCH64;
137 | }
138 | }
139 |
140 | throw new IllegalArgumentException();
141 | }
142 |
143 | public boolean is64Bit() {
144 | return is64Bit;
145 | }
146 |
147 | public String getCheckedName() {
148 | return checkedName;
149 | }
150 |
151 | public String getDisplayName() {
152 | return displayName;
153 | }
154 |
155 | @Override
156 | public String toString() {
157 | return displayName;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/platform/JAppRuntimeContext.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.platform;
17 |
18 | import org.glavo.japp.launcher.JAppConfigGroup;
19 |
20 | public final class JAppRuntimeContext {
21 |
22 | public static JAppRuntimeContext search(JAppConfigGroup config) {
23 | for (JavaRuntime java : JavaRuntime.getAllJava()) {
24 | JAppRuntimeContext context = new JAppRuntimeContext(java);
25 | if (config.canApply(context)) {
26 | return context;
27 | }
28 | }
29 |
30 | return null;
31 | }
32 |
33 | private final JavaRuntime java;
34 |
35 | public JAppRuntimeContext(JavaRuntime java) {
36 | this.java = java;
37 | }
38 |
39 | public JavaRuntime getJava() {
40 | return java;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/platform/LibC.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.platform;
17 |
18 | import java.util.Locale;
19 |
20 | public enum LibC {
21 | DEFAULT, MUSL;
22 |
23 | public static LibC parseLibC(String value) {
24 | switch (value) {
25 | case "":
26 | case "default":
27 | case "gnu":
28 | return DEFAULT;
29 | case "musl":
30 | return MUSL;
31 | default:
32 | throw new IllegalArgumentException(value);
33 | }
34 | }
35 |
36 | private final String checkedName = this.name().toLowerCase(Locale.ROOT);
37 |
38 | public String getCheckedName() {
39 | return checkedName;
40 | }
41 |
42 | @Override
43 | public String toString() {
44 | return getCheckedName();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/org/glavo/japp/platform/OperatingSystem.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.platform;
17 |
18 | import java.io.IOException;
19 | import java.nio.file.Path;
20 | import java.util.Locale;
21 |
22 | public enum OperatingSystem {
23 | WINDOWS("Windows"),
24 | LINUX("Linux"),
25 | MACOS("macOS");
26 |
27 | private final String checkedName;
28 | private final String displayName;
29 |
30 | OperatingSystem(String displayName) {
31 | this.checkedName = this.name().toLowerCase(Locale.ROOT);
32 | this.displayName = displayName;
33 | }
34 |
35 | public static OperatingSystem parseOperatingSystem(String name) {
36 | name = name.trim().toLowerCase(Locale.ROOT);
37 | if (name.contains("mac") || name.contains("darwin"))
38 | return MACOS;
39 | else if (name.contains("win"))
40 | return WINDOWS;
41 | else if (name.contains("linux"))
42 | return LINUX;
43 | else
44 | throw new IllegalArgumentException(name);
45 | }
46 |
47 | public String getCheckedName() {
48 | return checkedName;
49 | }
50 |
51 | public String getDisplayName() {
52 | return displayName;
53 | }
54 |
55 | public Path findJavaExecutable(Path javaHome) throws IOException {
56 | if (this == WINDOWS) {
57 | return javaHome.resolve("bin/java.exe");
58 | } else {
59 | return javaHome.resolve("bin/java");
60 | }
61 | }
62 |
63 | @Override
64 | public String toString() {
65 | return displayName;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/resources/org/glavo/japp/packer/header.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # TODO: In future we should look for japp launcher in the PATH
4 | exec "%japp.project.directory%/bin/japp.sh" run "${BASH_SOURCE[0]}" "$@"
5 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/boot/JAppResourceTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot;
17 |
18 | public class JAppResourceTest {
19 |
20 | public void test() {
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/boot/decompressor/zstd/ZstdTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.boot.decompressor.zstd;
17 |
18 | import com.github.luben.zstd.Zstd;
19 | import org.junit.jupiter.api.Test;
20 |
21 | import java.nio.ByteBuffer;
22 | import java.nio.file.Paths;
23 | import java.util.Arrays;
24 | import java.util.Collections;
25 | import java.util.Random;
26 | import java.util.zip.ZipEntry;
27 | import java.util.zip.ZipFile;
28 |
29 | import static org.junit.jupiter.api.Assertions.*;
30 |
31 | public class ZstdTest {
32 |
33 | private static void testDecompress(byte[] bytes, boolean checksum) throws Throwable {
34 | byte[] compressed;
35 | if (checksum) {
36 | byte[] tmp = new byte[bytes.length * 2 + 128];
37 | long len = Zstd.compress(tmp, bytes, Zstd.defaultCompressionLevel(), true);
38 | compressed = Arrays.copyOf(tmp, Math.toIntExact(len));
39 | } else {
40 | compressed = Zstd.compress(bytes);
41 | }
42 |
43 | ZstdFrameDecompressor decompressor = new ZstdFrameDecompressor();
44 |
45 | byte[] decompressed = new byte[bytes.length];
46 | int decompressedLen = decompressor.decompress(compressed, 0, compressed.length, decompressed, 0, decompressed.length);
47 | assertArrayEquals(bytes, decompressed);
48 | assertEquals(bytes.length, decompressedLen);
49 |
50 | Arrays.fill(decompressed, (byte) 0);
51 | ByteBuffer compressedBuffer = ByteBuffer.wrap(compressed);
52 | ByteBuffer decompressedBuffer = ByteBuffer.wrap(decompressed);
53 |
54 | decompressedLen = decompressor.decompress(compressedBuffer, decompressedBuffer);
55 | assertFalse(compressedBuffer.hasRemaining());
56 | assertFalse(decompressedBuffer.hasRemaining());
57 | assertArrayEquals(bytes, decompressed);
58 | assertEquals(bytes.length, decompressedLen);
59 |
60 | compressedBuffer = ByteBuffer.allocateDirect(compressed.length);
61 | compressedBuffer.put(compressed);
62 | compressedBuffer.flip();
63 | decompressedBuffer = ByteBuffer.allocateDirect(bytes.length);
64 |
65 | decompressedLen = decompressor.decompress(compressedBuffer, decompressedBuffer);
66 | assertFalse(compressedBuffer.hasRemaining());
67 | assertFalse(decompressedBuffer.hasRemaining());
68 |
69 | decompressedBuffer.flip();
70 | decompressedBuffer.get(decompressed);
71 |
72 | assertArrayEquals(bytes, decompressed);
73 | assertEquals(bytes.length, decompressedLen);
74 | }
75 |
76 | @Test
77 | void testDecompressor() throws Throwable {
78 | for (int len = 0; len <= 128; len++) {
79 | for (int seed = 0; seed < 100; seed++) {
80 | byte[] bytes = new byte[len];
81 | new Random(seed).nextBytes(bytes);
82 | try {
83 | testDecompress(bytes, false);
84 | testDecompress(bytes, true);
85 | } catch (Throwable e) {
86 | throw new AssertionError(String.format("seed=%s, len=%s", seed, len), e);
87 | }
88 | }
89 | }
90 |
91 | try (ZipFile zf = new ZipFile(Paths.get(Zstd.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile())) {
92 | for (ZipEntry entry : Collections.list(zf.entries())) {
93 | if (!entry.isDirectory()) {
94 | byte[] bytes = zf.getInputStream(entry).readAllBytes();
95 | try {
96 | testDecompress(bytes, false);
97 | testDecompress(bytes, true);
98 | } catch (Throwable e) {
99 | throw new AssertionError("entry=" + entry.getName());
100 | }
101 | }
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/classfile/ByteArrayPoolTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.classfile;
17 |
18 | import org.glavo.japp.boot.decompressor.classfile.ByteArrayPool;
19 | import org.glavo.japp.packer.compressor.classfile.ByteArrayPoolBuilder;
20 | import org.junit.jupiter.api.Test;
21 |
22 | import java.io.IOException;
23 | import java.nio.ByteBuffer;
24 | import java.util.Arrays;
25 |
26 | import static java.nio.charset.StandardCharsets.US_ASCII;
27 | import static org.junit.jupiter.api.Assertions.assertArrayEquals;
28 | import static org.junit.jupiter.api.Assertions.assertEquals;
29 |
30 | public class ByteArrayPoolTest {
31 |
32 | private static byte[] testString(int index) {
33 | return ("str" + index).getBytes(US_ASCII);
34 | }
35 |
36 | void test(int n) throws IOException {
37 | ByteArrayPoolBuilder builder = new ByteArrayPoolBuilder();
38 |
39 | for (int i = 0; i < n; i++) {
40 | assertEquals(i, builder.add(testString(i)));
41 | assertEquals(i, builder.add(testString(i)));
42 | }
43 |
44 | ByteArrayPool pool = builder.toPool();
45 |
46 | for (int i = 0; i < n; i++) {
47 | byte[] testString = testString(i);
48 |
49 | ByteBuffer buffer = ByteBuffer.allocate(20);
50 | assertEquals(testString.length, pool.get(i, buffer));
51 | assertEquals(testString.length, buffer.position());
52 |
53 | byte[] out = new byte[testString.length];
54 | buffer.flip().get(out);
55 | assertArrayEquals(testString, out);
56 |
57 | Arrays.fill(out, (byte) 0);
58 | pool.get(i).get(out);
59 | assertArrayEquals(testString, out);
60 | }
61 | }
62 |
63 | @Test
64 | void test() throws IOException {
65 | test(0);
66 | test(10);
67 | test(100);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/launcher/EndZipTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.launcher;
17 |
18 | import com.github.luben.zstd.Zstd;
19 | import org.glavo.japp.io.IOUtils;
20 | import org.junit.jupiter.api.Test;
21 | import org.junit.jupiter.params.ParameterizedTest;
22 | import org.junit.jupiter.params.provider.MethodSource;
23 |
24 | import java.io.IOException;
25 | import java.net.URISyntaxException;
26 | import java.nio.ByteBuffer;
27 | import java.nio.ByteOrder;
28 | import java.nio.channels.FileChannel;
29 | import java.nio.file.Path;
30 | import java.util.stream.Stream;
31 |
32 | import static org.junit.jupiter.api.Assertions.assertEquals;
33 |
34 | public class EndZipTest {
35 |
36 | private static Stream jars() {
37 | return Stream.of(Test.class, Zstd.class).map(clazz -> {
38 | try {
39 | return Path.of(clazz.getProtectionDomain().getCodeSource().getLocation().toURI());
40 | } catch (URISyntaxException e) {
41 | throw new AssertionError(e);
42 | }
43 | });
44 | }
45 |
46 | @ParameterizedTest
47 | @MethodSource("jars")
48 | void testGetSize(Path zipFile) throws IOException {
49 | try (FileChannel channel = FileChannel.open(zipFile)) {
50 | long fileSize = channel.size();
51 |
52 | int endBufferSize = (int) Math.max(fileSize, 8192);
53 | ByteBuffer endBuffer = ByteBuffer.allocate(endBufferSize).order(ByteOrder.LITTLE_ENDIAN);
54 |
55 | channel.position(fileSize - endBufferSize);
56 | IOUtils.readFully(channel, endBuffer);
57 |
58 | endBuffer.flip();
59 |
60 | assertEquals(fileSize, JAppLauncherMetadata.getEndZipSize(endBuffer));
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/packer/ModuleInfoReaderTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer;
17 |
18 | import com.github.luben.zstd.Zstd;
19 | import org.junit.jupiter.api.Test;
20 | import org.junit.jupiter.params.ParameterizedTest;
21 | import org.junit.jupiter.params.provider.Arguments;
22 | import org.junit.jupiter.params.provider.MethodSource;
23 |
24 | import java.io.ByteArrayInputStream;
25 | import java.io.File;
26 | import java.io.IOException;
27 | import java.io.InputStream;
28 | import java.util.Map;
29 | import java.util.stream.Stream;
30 | import java.util.zip.ZipFile;
31 |
32 | import static org.junit.jupiter.api.Assertions.assertEquals;
33 |
34 | public final class ModuleInfoReaderTest {
35 | @Test
36 | public void deriveAutomaticModuleNameTest() {
37 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("a.jar"));
38 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("a-0.1.0.jar"));
39 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("...a-0.1.0.jar"));
40 | assertEquals("a", ModuleInfoReader.deriveAutomaticModuleName("...a...-0.1.0.jar"));
41 | assertEquals("a.b", ModuleInfoReader.deriveAutomaticModuleName("a-b-0.1.0.jar"));
42 | assertEquals("a.b", ModuleInfoReader.deriveAutomaticModuleName("a--b-0.1.0.jar"));
43 | }
44 |
45 | static Stream readModuleNameTestArguments() {
46 | return Map.of(
47 | "org.junit.jupiter.api", Test.class,
48 | "com.github.luben.zstd_jni", Zstd.class
49 | ).entrySet().stream().map(entry -> {
50 |
51 | byte[] moduleInfo;
52 | try (ZipFile zipFile = new ZipFile(new File(entry.getValue().getProtectionDomain().getCodeSource().getLocation().toURI()))) {
53 | try (InputStream inputStream = zipFile.getInputStream(zipFile.getEntry("module-info.class"))) {
54 | moduleInfo = inputStream.readAllBytes();
55 | }
56 | } catch (Exception e) {
57 | throw new AssertionError(e);
58 | }
59 |
60 | return Arguments.of(entry.getKey(), moduleInfo);
61 | });
62 | }
63 |
64 | @ParameterizedTest
65 | @MethodSource("readModuleNameTestArguments")
66 | public void readModuleNameTest(String moduleName, byte[] moduleInfo) throws IOException {
67 | assertEquals(moduleName, ModuleInfoReader.readModuleName(new ByteArrayInputStream(moduleInfo)));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/packer/processor/PathListParserTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.packer.processor;
17 |
18 | import org.junit.jupiter.api.Assertions;
19 | import org.junit.jupiter.api.Test;
20 |
21 | import java.io.File;
22 | import java.util.ArrayList;
23 | import java.util.List;
24 | import java.util.Map;
25 |
26 | public class PathListParserTest {
27 | static final String PS = File.pathSeparator;
28 |
29 | private record Pair(Map options, String path) {
30 | }
31 |
32 | private static List parse(String list) {
33 | List res = new ArrayList<>();
34 | PathListParser parser = new PathListParser(list);
35 | while (parser.scanNext()) {
36 | res.add(new Pair(parser.options, parser.path));
37 | }
38 | return res;
39 | }
40 |
41 | @Test
42 | public void test() {
43 | Assertions.assertEquals(List.of(), parse(""));
44 | Assertions.assertEquals(List.of(), parse(PS));
45 | Assertions.assertEquals(List.of(), parse(PS + PS));
46 | Assertions.assertEquals(
47 | List.of(new Pair(Map.of(), "A"), new Pair(Map.of(), "B"), new Pair(Map.of(), "C")),
48 | parse(String.join(PS, "A", "B", "", "C", ""))
49 | );
50 | Assertions.assertEquals(
51 | List.of(new Pair(Map.of(), "A"),
52 | new Pair(Map.of("type", "maven", "repo", "local"), "B"),
53 | new Pair(Map.of("type", "local"), "C"),
54 | new Pair(Map.of("type", "local"), "")),
55 | parse(String.join(PS, "[]A", "[type=maven,repo=local]B", "[type='local']C", "[type=local]", ""))
56 | );
57 |
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/testcase/HelloWorldTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.testcase;
17 |
18 | import java.util.List;
19 | import java.util.stream.Stream;
20 |
21 | public final class HelloWorldTest implements JAppTestTemplate {
22 | public static final String FILE = JAppTestHelper.getTestCase("helloworld");
23 | public static final String MAIN_CLASS = "org.glavo.japp.testcase.helloworld.HelloWorld";
24 |
25 | @Override
26 | public Stream tests() {
27 | return Stream.of(
28 | newTest("module path", List.of("--module-path", FILE, MAIN_CLASS), List.of("Hello World!")),
29 | newTest("classpath", List.of("--classpath", FILE, MAIN_CLASS), List.of("Hello World!")),
30 | newTest("module path with end zip", List.of("--embed-launcher", "--module-path", FILE, MAIN_CLASS), List.of("Hello World!"))
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/testcase/JAppTestHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.testcase;
17 |
18 | import org.junit.jupiter.api.Assertions;
19 |
20 | import java.io.Closeable;
21 | import java.io.IOException;
22 | import java.nio.charset.StandardCharsets;
23 | import java.nio.file.Files;
24 | import java.nio.file.Path;
25 | import java.util.ArrayList;
26 | import java.util.Collections;
27 | import java.util.List;
28 |
29 | public final class JAppTestHelper {
30 | private static final String jar = System.getProperty("japp.jar");
31 | private static final boolean isWindows = System.getProperty("os.name").startsWith("Win");
32 |
33 | private static String runJApp(String mode, List args) throws IOException {
34 | ArrayList list = new ArrayList<>();
35 | list.add(System.getProperty("java.home") + (isWindows ? "\\bin\\java.exe" : "/bin/java"));
36 | list.add("-Dsun.stdout.encoding=UTF-8");
37 | list.add("-Dsun.stderr.encoding=UTF-8");
38 | list.add("-Dstdout.encoding=UTF-8");
39 | list.add("-Dstderr.encoding=UTF-8");
40 | list.add("-jar");
41 | list.add(jar);
42 | list.add(mode);
43 | list.addAll(args);
44 |
45 | try {
46 | Process process = Runtime.getRuntime().exec(list.toArray(new String[0]));
47 | int res = process.waitFor();
48 | if (res != 0) {
49 | throw new RuntimeException("Process exit code is " + res + ", stderr=" +
50 | new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8));
51 | }
52 | return new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
53 | } catch (InterruptedException e) {
54 | throw new AssertionError(e);
55 | }
56 | }
57 |
58 | public static final class FileHolder implements Closeable {
59 |
60 | public final Path file;
61 |
62 | public FileHolder(Path file) {
63 | this.file = file;
64 | }
65 |
66 | @Override
67 | public void close() throws IOException {
68 | Files.deleteIfExists(file);
69 | }
70 | }
71 |
72 | public static String getTestCase(String name) {
73 | String file = System.getProperty("japp.testcase." + name);
74 | if (file == null) {
75 | throw new AssertionError(name);
76 | }
77 |
78 | return file;
79 | }
80 |
81 | public static FileHolder create(String... args) throws IOException {
82 | Path targetFile = Files.createTempFile("japp-test-", ".japp").toAbsolutePath();
83 |
84 | ArrayList argsList = new ArrayList<>();
85 | argsList.add("-o");
86 | argsList.add(targetFile.toString());
87 | Collections.addAll(argsList, args);
88 | runJApp("create", argsList);
89 |
90 | return new FileHolder(targetFile);
91 | }
92 |
93 | public static String launch(Path file, String... args) throws IOException {
94 | ArrayList argsList = new ArrayList<>();
95 | argsList.add(file.toAbsolutePath().normalize().toString());
96 | Collections.addAll(argsList, args);
97 | return runJApp("run", argsList);
98 | }
99 |
100 | public static void assertLines(String value, String... lines) throws IOException {
101 | StringBuilder builder = new StringBuilder();
102 | for (String line : lines) {
103 | builder.append(line).append(System.lineSeparator());
104 | }
105 |
106 | Assertions.assertEquals(builder.toString(), value);
107 | }
108 |
109 | public static void test(List args, List lines) throws IOException {
110 | try (JAppTestHelper.FileHolder holder = JAppTestHelper.create(args.toArray(String[]::new))) {
111 | assertLines(JAppTestHelper.launch(holder.file), lines.toArray(String[]::new));
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/testcase/JAppTestTemplate.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.testcase;
17 |
18 | import org.junit.jupiter.api.DynamicTest;
19 | import org.junit.jupiter.api.TestFactory;
20 |
21 | import java.util.List;
22 | import java.util.stream.Stream;
23 |
24 | public interface JAppTestTemplate {
25 | Stream tests();
26 |
27 | default TestArgument newTest(String name, List argument, List lines) {
28 | return new TestArgument(name, argument, lines);
29 | }
30 |
31 | @TestFactory
32 | default Stream testFactory() {
33 | return tests().map(argument -> DynamicTest.dynamicTest(argument.name, () -> {
34 | try (JAppTestHelper.FileHolder holder = JAppTestHelper.create(argument.argument.toArray(String[]::new))) {
35 | JAppTestHelper.assertLines(JAppTestHelper.launch(holder.file), argument.lines.toArray(String[]::new));
36 | }
37 | }));
38 | }
39 |
40 | class TestArgument {
41 | public final String name;
42 | public final List argument;
43 | public final List lines;
44 |
45 | TestArgument(String name, List argument, List lines) {
46 | this.name = name;
47 | this.argument = argument;
48 | this.lines = lines;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/testcase/ModulePathTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.testcase;
17 |
18 | import java.util.List;
19 | import java.util.stream.Stream;
20 |
21 | public final class ModulePathTest implements JAppTestTemplate {
22 |
23 | public static final String FILE = JAppTestHelper.getTestCase("modulepath");
24 |
25 | @Override
26 | public Stream tests() {
27 | return Stream.of(
28 | newTest("test",
29 | List.of("--module-path", FILE, "org.glavo.japp.testcase.modulepath.ModulePath"),
30 | List.of(
31 | "japp:/modules/org.glavo.japp.testcase.modulepath/org/glavo/japp/testcase/modulepath/ModulePath.class",
32 | "japp:/modules/com.google.gson/com/google/gson/Gson.class",
33 | "japp:/modules/org.apache.commons.lang3/org/apache/commons/lang3/ObjectUtils.class"
34 | )
35 | )
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/util/CompressedNumberTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import org.junit.jupiter.api.Assertions;
19 | import org.junit.jupiter.api.Test;
20 |
21 | import java.nio.ByteBuffer;
22 | import java.util.Random;
23 |
24 | public class CompressedNumberTest {
25 |
26 | private static void testCompressInt(int v) {
27 | ByteBuffer buffer = ByteBuffer.allocate(5);
28 | CompressedNumber.putInt(buffer, v);
29 | buffer.flip();
30 | int cv = CompressedNumber.getInt(buffer);
31 |
32 | Assertions.assertEquals(v, cv);
33 | }
34 |
35 | @Test
36 | void testCompressInt() {
37 | for (int i = 0; i <= 256; i++) {
38 | testCompressInt(i);
39 | }
40 |
41 | Random random = new Random(0);
42 | for (int i = 0; i < 256; i++) {
43 | testCompressInt(random.nextInt(Integer.MAX_VALUE));
44 | }
45 |
46 | testCompressInt(Integer.MAX_VALUE);
47 |
48 | Assertions.assertThrows(AssertionError.class, () -> testCompressInt(-1));
49 | Assertions.assertThrows(AssertionError.class, () -> testCompressInt(-Integer.MAX_VALUE));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/util/MUTF8Test.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2024 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import org.junit.jupiter.params.ParameterizedTest;
19 | import org.junit.jupiter.params.provider.MethodSource;
20 |
21 | import java.io.ByteArrayOutputStream;
22 | import java.io.DataOutputStream;
23 | import java.io.IOException;
24 | import java.util.Arrays;
25 | import java.util.stream.Stream;
26 |
27 | import static org.junit.jupiter.api.Assertions.assertEquals;
28 |
29 | public final class MUTF8Test {
30 | private static Stream strs() {
31 | return Stream.of(
32 | "",
33 | "\0\0\0",
34 | "Hello World!",
35 | "Hello àáâãäå",
36 | "Hello 测试字符串",
37 | "测试测试ABC测试测试\0测试"
38 | );
39 | }
40 |
41 | @ParameterizedTest
42 | @MethodSource("strs")
43 | public void testDecode(String str) throws IOException {
44 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
45 | try (DataOutputStream dataOutput = new DataOutputStream(byteArrayOutputStream)) {
46 | dataOutput.writeUTF(str);
47 | }
48 |
49 | byte[] arr = byteArrayOutputStream.toByteArray();
50 |
51 | assertEquals(str, MUTF8.stringFromMUTF8(Arrays.copyOfRange(arr, 2, arr.length)));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/java/org/glavo/japp/util/XxHash64Test.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.util;
17 |
18 | import org.junit.jupiter.params.ParameterizedTest;
19 | import org.junit.jupiter.params.provider.MethodSource;
20 |
21 | import java.nio.ByteBuffer;
22 | import java.util.Random;
23 | import java.util.stream.IntStream;
24 |
25 | import static org.junit.jupiter.api.Assertions.assertEquals;
26 |
27 | public class XxHash64Test {
28 | private static IntStream testArguments() {
29 | return IntStream.concat(
30 | IntStream.rangeClosed(0, 32),
31 | IntStream.iterate(33, it -> it < 512, it -> it + 7)
32 | ).flatMap(it -> IntStream.of(it, it + 8192));
33 | }
34 |
35 | @ParameterizedTest
36 | @MethodSource("testArguments")
37 | public void test(int length) {
38 | byte[] data = new byte[length];
39 | new Random(0).nextBytes(data);
40 |
41 | ByteBuffer nativeBuffer = ByteBuffer.allocateDirect(length);
42 | nativeBuffer.put(data);
43 | nativeBuffer.position(0);
44 |
45 | long expected = org.lwjgl.util.xxhash.XXHash.XXH64(nativeBuffer, 0L);
46 | nativeBuffer.position(0);
47 |
48 | assertEquals(expected, XxHash64.hashByteBufferWithoutUpdate(ByteBuffer.wrap(data)));
49 | assertEquals(expected, XxHash64.hashByteBufferWithoutUpdate(nativeBuffer));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test-case/HelloWorld/build.gradle.kts:
--------------------------------------------------------------------------------
1 | tasks.compileJava {
2 | options.release.set(9)
3 | }
--------------------------------------------------------------------------------
/test-case/HelloWorld/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | module org.glavo.japp.testcase.helloworld {
2 | }
--------------------------------------------------------------------------------
/test-case/HelloWorld/src/main/java/org/glavo/japp/testcase/helloworld/HelloWorld.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.testcase.helloworld;
17 |
18 | public final class HelloWorld {
19 | public static void main(String[] args) {
20 | System.out.println("Hello World!");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/test-case/ModulePath/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | // https://mvnrepository.com/artifact/com.google.code.gson/gson
3 | implementation("com.google.code.gson:gson:2.10.1")
4 |
5 | // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
6 | implementation("org.apache.commons:commons-lang3:3.14.0")
7 | }
8 |
9 | tasks.jar {
10 | manifest.attributes(
11 | "Automatic-Module-Name" to "org.glavo.japp.testcase.modulepath"
12 | )
13 | }
--------------------------------------------------------------------------------
/test-case/ModulePath/src/main/java/org/glavo/japp/testcase/modulepath/ModulePath.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Glavo
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.glavo.japp.testcase.modulepath;
17 |
18 | import com.google.gson.Gson;
19 | import org.apache.commons.lang3.ObjectUtils;
20 |
21 | public final class ModulePath {
22 | public static void main(String[] args) {
23 | System.out.println(ModulePath.class.getResource("ModulePath.class"));
24 | System.out.println(Gson.class.getResource("Gson.class"));
25 | System.out.println(ObjectUtils.class.getResource("ObjectUtils.class"));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------