Skip to content

Commit

Permalink
Add java bytecode class file version detection
Browse files Browse the repository at this point in the history
Signed-off-by: Jorge Solórzano <[email protected]>
  • Loading branch information
jorsol authored and rfscholte committed Sep 16, 2023
1 parent 537321d commit 5c06f1b
Show file tree
Hide file tree
Showing 20 changed files with 276 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.codehaus.plexus.languages.java.version;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Reads the bytecode of a Java class to detect the major, minor and Java
* version that was compiled.
*
* @author Jorge Solórzano
*/
public final class JavaClassfileVersion {

private final int major;
private final int minor;

JavaClassfileVersion(int major, int minor) {
if (major < 45) {
throw new IllegalArgumentException("Java class major version must be 45 or above.");
}
this.major = major;
this.minor = minor;
}

/**
* Reads the bytecode of a Java class file and returns the
* {@link JavaClassfileVersion}.
*
* @param bytes {@code byte[]} of the Java class file
* @return the {@link JavaClassfileVersion} of the byte array
*/
public static JavaClassfileVersion of(byte[] bytes) {
return JavaClassfileVersionParser.of(bytes);
}

/**
* Reads the bytecode of a Java class file and returns the
* {@link JavaClassfileVersion}.
*
* @param path {@link Path} of the Java class file
* @return the {@link JavaClassfileVersion} of the path java class
*/
public static JavaClassfileVersion of(Path path) {
try {
byte[] readAllBytes = Files.readAllBytes(path);
return of(readAllBytes);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}

/**
* JavaVersion of the class file version detected.
*
* @return JavaVersion based on the major version of the class file.
*/
public JavaVersion javaVersion() {
int javaVer = major - 44;
String javaVersion = javaVer < 9 ? "1." + javaVer : Integer.toString(javaVer);

return JavaVersion.parse(javaVersion);
}

/**
* Returns the major version of the parsed classfile.
*
* @return the major classfile version
*/
public int majorVersion() {
return major;
}

/**
* Returns the minor version of the parsed classfile.
*
* @return the minor classfile version
*/
public int minorVersion() {
return minor;
}

/**
* Returns if the classfile use preview features.
*
* @return {@code true} if the classfile use preview features.
*/
public boolean isPreview() {
return minor == 65535;
}

/**
* Returns a String representation of the Java class file version, e.g.
* {@code 65.0 (Java 21)}.
*
* @return String representation of the Java class file version
*/
@Override
public String toString() {
return major + "." + minor + " (Java " + javaVersion() + ")";
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + major;
result = prime * result + minor;
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JavaClassfileVersion)) return false;
JavaClassfileVersion other = (JavaClassfileVersion) obj;
if (major != other.major) return false;
if (minor != other.minor) return false;
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.codehaus.plexus.languages.java.version;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.UncheckedIOException;

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* This class is intented to be package-private and consumed by
* {@link JavaClassfileVersion}.
*
* @author Jorge Solórzano
*/
final class JavaClassfileVersionParser {

private JavaClassfileVersionParser() {}

/**
* Reads the bytecode of a Java class file and returns the {@link JavaClassfileVersion}.
*
* @param in {@code byte[]} of the Java class file
* @return the {@link JavaClassfileVersion} of the input stream
*/
public static JavaClassfileVersion of(byte[] bytes) {
try (final DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes))) {
if (0xCAFEBABE != data.readInt()) {
throw new IOException("Invalid java class file header");
}
int minor = data.readUnsignedShort();
int major = data.readUnsignedShort();
return new JavaClassfileVersion(major, minor);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private JavaVersion(String rawVersion, boolean isMajor) {
* Actual parsing is done when calling {@link #compareTo(JavaVersion)}
*
* @param s the version string, never {@code null}
* @return the version wrapped in a JavadocVersion
* @return the version wrapped in a JavaVersion
*/
public static JavaVersion parse(String s) {
return new JavaVersion(s, !s.startsWith("1."));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.codehaus.plexus.languages.java.version;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class JavaClassVersionTest {

@ParameterizedTest
@MethodSource("provideClassFiles")
void testFilesClassVersions(Path filePath) {
String fileName = filePath.getFileName().toString();
int javaVersion = Integer.valueOf(fileName.substring(fileName.indexOf("-") + 1, fileName.length() - 6));
JavaClassfileVersion classVersion = JavaClassfileVersion.of(filePath);
assertEquals(javaVersion + 44, classVersion.majorVersion());
assertEquals(0, classVersion.minorVersion());
assertEquals(JavaVersion.parse("" + javaVersion), classVersion.javaVersion());
}

static Stream<Path> provideClassFiles() {
List<Path> paths;
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(Paths.get("src/test/resources/classfile.version/"), "*-[0-9]?.class")) {
paths = StreamSupport.stream(directoryStream.spliterator(), false)
.filter(Files::isRegularFile)
.collect(Collectors.toList());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return paths.stream();
}

@Test
void testJavaClassPreview() {
Path previewFile = Paths.get("src/test/resources/classfile.version/helloworld-preview.class");
JavaClassfileVersion previewClass = JavaClassfileVersion.of(previewFile);
assertTrue(previewClass.isPreview());
assertEquals(20 + 44, previewClass.majorVersion());
assertEquals(JavaVersion.parse("20"), previewClass.javaVersion());
}

@Test
void testJavaClassVersionMajor45orAbove() {
assertThrows(
IllegalArgumentException.class,
() -> new JavaClassfileVersion(44, 0),
"Java class major version must be 45 or above.");
}

@Test
void equalsContract() {
JavaClassfileVersion javaClassVersion = new JavaClassfileVersion(65, 0);
JavaClassfileVersion previewFeature = new JavaClassfileVersion(65, 65535);
assertNotEquals(javaClassVersion, previewFeature);
assertNotEquals(javaClassVersion.hashCode(), previewFeature.hashCode());

JavaClassfileVersion javaClassVersionOther = new JavaClassfileVersion(65, 0);
assertEquals(javaClassVersion, javaClassVersionOther);
assertEquals(javaClassVersion.hashCode(), javaClassVersionOther.hashCode());
assertEquals(javaClassVersion.javaVersion(), javaClassVersionOther.javaVersion());
assertEquals(javaClassVersion.javaVersion(), previewFeature.javaVersion());
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 5c06f1b

Please sign in to comment.