Skip to content

Commit

Permalink
Parse module name out of .beam files
Browse files Browse the repository at this point in the history
  • Loading branch information
KronicDeth committed Jan 1, 2017
1 parent 627b8ad commit 4bb71f9
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/tmp
# user-specific project metadata
/.idea/dictionaries/
/.idea/markdown-navigator*
/.idea/workspace.xml
/.idea/shelf/
# ignore idea.xml output
Expand Down
5 changes: 4 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/runConfigurations/org_elixir_lang.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 105 additions & 0 deletions src/org/elixir_lang/beam/Beam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.elixir_lang.beam;

import com.ericsson.otp.erlang.OtpErlangDecodeException;
import gnu.trove.THashMap;
import org.elixir_lang.beam.chunk.Atoms;
import org.elixir_lang.beam.chunk.Chunk;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static org.elixir_lang.beam.chunk.Chunk.TypeID.ATOM;
import static org.elixir_lang.beam.chunk.Chunk.length;
import static org.elixir_lang.beam.chunk.Chunk.typeID;

/**
* See http://beam-wisdoms.clau.se/en/latest/indepth-beam-file.html
*/
public class Beam {
/*
* CONSTANTS
*/

private static final String HEADER = "FOR1";

/*
* Fields
*/

private Map<String, Chunk> chunkByTypeID;

/*
* Constructors
*/

private Beam(@NotNull Collection<Chunk> chunkCollection) {
chunkByTypeID = new THashMap<String, Chunk>(chunkCollection.size());

for (Chunk chunk : chunkCollection) {
chunkByTypeID.put(chunk.typeID, chunk);
}
}

/*
* Static Methods
*/

@Nullable
public static Beam from(@NotNull DataInputStream dataInputStream) throws IOException, OtpErlangDecodeException {
if (!HEADER.equals(typeID(dataInputStream))) {
return null;
}

length(dataInputStream);

if (!"BEAM".equals(typeID(dataInputStream))) {
return null;
}

List<Chunk> chunkList = new ArrayList<Chunk>();
while (true) {
Chunk chunk = Chunk.from(dataInputStream);

if (chunk != null) {
chunkList.add(chunk);
} else {
break;
}
}

return new Beam(chunkList);
}

/*
* Instance Methods
*/

@Nullable
public Atoms atoms() {
Atoms atoms = null;

Chunk chunk = chunk(ATOM);

if (chunk != null) {
atoms = Atoms.from(chunk);
}

return atoms;
}

@Nullable
private Chunk chunk(@NotNull String typeID) {
return chunkByTypeID.get(typeID);
}

@Nullable
private Chunk chunk(@NotNull Chunk.TypeID typeID) {
return chunk(typeID.toString());
}
}
73 changes: 73 additions & 0 deletions src/org/elixir_lang/beam/chunk/Atoms.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.elixir_lang.beam.chunk;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.elixir_lang.beam.chunk.Chunk.TypeID.ATOM;
import static org.elixir_lang.beam.chunk.Chunk.unsignedByte;
import static org.elixir_lang.beam.chunk.Chunk.unsignedInt;

public class Atoms {
/*
* Fields
*/

@NotNull
private List<String> atomList;

/*
* Constructors
*/

public Atoms(@NotNull List<String> atomList) {
this.atomList = Collections.unmodifiableList(atomList);
}

/*
* Static Methods
*/

@Nullable
public static Atoms from(@NotNull Chunk chunk) {
Atoms atoms = null;

if (chunk.typeID.equals(ATOM.toString()) && chunk.data.length >= 4) {
int offset = 0;
long atomCount = unsignedInt(chunk.data, offset);
List<String> atomList = new ArrayList<String>();
offset += 4;

for (long i = 0; i < atomCount; i++) {
int atomLength = unsignedByte(chunk.data[offset]);
offset += 1;

String entry = new String(chunk.data, offset, atomLength);
offset += atomLength;
atomList.add(entry);
}

atoms = new Atoms(atomList);
}

return atoms;
}

/*
* Instance Methods
*/

@Nullable
public String moduleName() {
String moduleName = null;

if (atomList.size() > 0) {
moduleName = atomList.get(0);
}

return moduleName;
}
}
105 changes: 105 additions & 0 deletions src/org/elixir_lang/beam/chunk/Chunk.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.elixir_lang.beam.chunk;

import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.DataInputStream;
import java.io.IOException;

/**
* Chunk of a `.beam` file. Same chunk format as base IFF
*/
public class Chunk {
private static final int ALIGNMENT = 4;
@NotNull
public final String typeID;
@NotNull
public final byte[] data;
private Chunk(@NotNull String typeID, @NotNull byte[] data) {
this.typeID = typeID;
this.data = data;
}

@Nullable
public static Chunk from(@NotNull DataInputStream dataInputStream) throws IOException {
String typeID = typeID(dataInputStream);
Chunk chunk = null;

if (typeID != null) {
long length = length(dataInputStream);

byte[] data = new byte[(int) length];
dataInputStream.readFully(data);

int padding = (int) ((ALIGNMENT - (length % ALIGNMENT)) % ALIGNMENT);
dataInputStream.skipBytes(padding);

chunk = new Chunk(typeID, data);
}

return chunk;
}

public static long length(@NotNull DataInputStream dataInputStream) throws IOException {
return readUnsignedInt(dataInputStream);
}

@Nullable
public static String typeID(@NotNull DataInputStream dataInputStream) throws IOException {
byte[] bytes = new byte[4];
int bytesRead = dataInputStream.read(bytes, 0, bytes.length);
String typeID = null;

if (bytesRead == bytes.length) {
typeID = new String(bytes);
}

return typeID;
}

private static long readUnsignedInt(@NotNull DataInputStream dataInputStream) throws IOException {
byte[] bytes = new byte[32 / 8];

dataInputStream.readFully(bytes);

return unsignedInt(bytes);
}

static int unsignedByte(byte signedByte) {
return signedByte & 0xFF;
}

static long unsignedInt(@NotNull byte[] bytes) {
return unsignedInt(bytes, 0);
}

@Contract(pure = true)
static long unsignedInt(@NotNull byte[] bytes, int offset) {
assert bytes.length >= offset + 4;

long unsignedInt = 0;

for (int i = 0; i < 4; i++) {
int unsignedByte = unsignedByte(bytes[offset + i]);
unsignedInt += unsignedByte << 8 * (4 - 1 - i);
}

return unsignedInt;
}

public enum TypeID {
ATOM("Atom");

private final String typeID;

TypeID(@NotNull String typeID) {
this.typeID = typeID;
}

@Override
public String toString() {
return typeID;
}
}
}
52 changes: 52 additions & 0 deletions tests/org/elixir_lang/beam/BeamTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.elixir_lang.beam;

import com.ericsson.otp.erlang.OtpErlangDecodeException;
import org.elixir_lang.beam.chunk.Atoms;
import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Test;

import java.io.*;

import static org.junit.Assert.*;

public class BeamTest {
@NotNull
private String ebinDirectory;

@Test
public void elixirModule() throws IOException, OtpErlangDecodeException {
assertModuleName("Elixir.Kernel", "Elixir.Kernel");
}

@Test
public void erlangModule() throws IOException, OtpErlangDecodeException {
assertModuleName("elixir_interpolation", "elixir_interpolation");
}

private void assertModuleName(@NotNull String baseName, @NotNull String moduleName) throws IOException, OtpErlangDecodeException {
DataInputStream dataInputStream = new DataInputStream(
new BufferedInputStream(
new FileInputStream(ebinDirectory + baseName + ".beam")
)
);
Beam beam = Beam.from(dataInputStream);

assertNotNull(beam);

Atoms atoms = beam.atoms();

assertNotNull(atoms);

assertEquals(atoms.moduleName(), moduleName);
}

@Before
public void setEbinDirectory() {
String ebinDirectory = System.getenv("ELIXIR_EBIN_DIRECTORY");

assertNotNull("ELIXIR_EBIN_DIRECTORY is not set", ebinDirectory);

this.ebinDirectory = ebinDirectory;
}
}

0 comments on commit 4bb71f9

Please sign in to comment.