From e96df3ef7e69ca0ff449dfc87828cd81b2af81ec Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 16 Nov 2017 15:11:36 +0300 Subject: [PATCH] Add java.util.jar.* classes --- .../classlib/java/util/jar/TAttributes.java | 234 ++++++++++++++++ .../classlib/java/util/jar/TInitManifest.java | 213 +++++++++++++++ .../classlib/java/util/jar/TJarEntry.java | 57 ++++ .../classlib/java/util/jar/TJarException.java | 29 ++ .../classlib/java/util/jar/TJarFile.java | 250 +++++++++++++++++ .../java/util/jar/TJarInputStream.java | 96 +++++++ .../java/util/jar/TJarOutputStream.java | 47 ++++ .../classlib/java/util/jar/TJarUtils.java | 58 ++++ .../classlib/java/util/jar/TManifest.java | 253 ++++++++++++++++++ 9 files changed, 1237 insertions(+) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TAttributes.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TInitManifest.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarEntry.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarException.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarFile.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarInputStream.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarOutputStream.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarUtils.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/util/jar/TManifest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TAttributes.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TAttributes.java new file mode 100644 index 000000000..0066d6660 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TAttributes.java @@ -0,0 +1,234 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class TAttributes implements Cloneable, Map { + protected Map map; + + public static class Name { + private final byte[] name; + private int hashCode; + + public static final Name CLASS_PATH = new Name("Class-Path"); + public static final Name MANIFEST_VERSION = new Name("Manifest-Version"); + public static final Name MAIN_CLASS = new Name("Main-Class"); + public static final Name SIGNATURE_VERSION = new Name("Signature-Version"); + public static final Name CONTENT_TYPE = new Name("Content-Type"); + public static final Name SEALED = new Name("Sealed"); + public static final Name IMPLEMENTATION_TITLE = new Name("Implementation-Title"); + public static final Name IMPLEMENTATION_VERSION = new Name("Implementation-Version"); + public static final Name IMPLEMENTATION_VENDOR = new Name("Implementation-Vendor"); + public static final Name SPECIFICATION_TITLE = new Name("Specification-Title"); + public static final Name SPECIFICATION_VERSION = new Name("Specification-Version"); + public static final Name SPECIFICATION_VENDOR = new Name("Specification-Vendor"); + public static final Name EXTENSION_LIST = new Name("Extension-List"); + public static final Name EXTENSION_NAME = new Name("Extension-Name"); + public static final Name EXTENSION_INSTALLATION = new Name("Extension-Installation"); + public static final Name IMPLEMENTATION_VENDOR_ID = new Name("Implementation-Vendor-Id"); + public static final Name IMPLEMENTATION_URL = new Name("Implementation-URL"); + + static final Name NAME = new Name("Name"); + + public Name(String s) { + int i = s.length(); + if (i == 0 || i > TManifest.LINE_LENGTH_LIMIT - 2) { + throw new IllegalArgumentException(); + } + + name = new byte[i]; + + for (; --i >= 0;) { + char ch = s.charAt(i); + if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') + || ch == '_' || ch == '-' || (ch >= '0' && ch <= '9'))) { + throw new IllegalArgumentException(s); + } + name[i] = (byte) ch; + } + } + + /** + * A private constructor for a trusted attribute name. + */ + Name(byte[] buf) { + name = buf; + } + + byte[] getBytes() { + return name; + } + + @Override + public String toString() { + try { + return new String(name, "ISO-8859-1"); //$NON-NLS-1$ + } catch (UnsupportedEncodingException iee) { + throw new InternalError(iee.getLocalizedMessage()); + } + } + + @Override + public boolean equals(Object object) { + if (object == null || object.getClass() != getClass() || object.hashCode() != hashCode()) { + return false; + } + + return TJarUtils.asciiEqualsIgnoreCase(name, ((Name) object).name); + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int hash = 0; + int multiplier = 1; + for (int i = name.length - 1; i >= 0; i--) { + // 'A' & 0xDF == 'a' & 0xDF, ..., 'Z' & 0xDF == 'z' & 0xDF + hash += (name[i] & 0xDF) * multiplier; + int shifted = multiplier << 5; + multiplier = shifted - multiplier; + } + hashCode = hash; + } + return hashCode; + } + + } + public TAttributes() { + map = new HashMap<>(); + } + + @SuppressWarnings("unchecked") + public TAttributes(TAttributes attrib) { + map = (Map) ((HashMap) attrib.map).clone(); + } + + public TAttributes(int size) { + map = new HashMap<>(size); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Set> entrySet() { + return map.entrySet(); + } + + @Override + public Object get(Object key) { + return map.get(key); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + @SuppressWarnings("cast") + // Require cast to force ClassCastException + public Object put(Object key, Object value) { + return map.put(key, value); + } + + @Override + public void putAll(Map attrib) { + if (attrib == null || !(attrib instanceof TAttributes)) { + throw new ClassCastException(); + } + this.map.putAll(attrib); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public Collection values() { + return map.values(); + } + + @SuppressWarnings("unchecked") + @Override + public Object clone() { + TAttributes clone; + try { + clone = (TAttributes) super.clone(); + } catch (CloneNotSupportedException e) { + return null; + } + clone.map = (Map) ((HashMap) map).clone(); + return clone; + } + + @Override + public int hashCode() { + return map.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TAttributes) { + return map.equals(((TAttributes) obj).map); + } + return false; + } + + public String getValue(Name name) { + return (String) map.get(name); + } + + public String getValue(String name) { + return (String) map.get(new Name(name)); + } + + public String putValue(String name, String val) { + return (String) map.put(new Name(name), val); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TInitManifest.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TInitManifest.java new file mode 100644 index 000000000..bf57e7727 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TInitManifest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.util.Map; + +class TInitManifest { + + private byte[] buf; + + private int pos; + + TAttributes.Name name; + + String value; + + CharsetDecoder decoder = Charset.defaultCharset().newDecoder(); + CharBuffer cBuf = CharBuffer.allocate(512); + + TInitManifest(byte[] buf, TAttributes main, TAttributes.Name ver) throws IOException { + this.buf = buf; + + // check a version attribute + if (!readHeader() || (ver != null && !name.equals(ver))) { + throw new IOException(); + } + + main.put(name, value); + while (readHeader()) { + main.put(name, value); + } + } + + void initEntries(Map entries, Map chunks) throws IOException { + + int mark = pos; + while (readHeader()) { + if (!TAttributes.Name.NAME.equals(name)) { + throw new IOException(); + } + String entryNameValue = value; + + TAttributes entry = entries.get(entryNameValue); + if (entry == null) { + entry = new TAttributes(12); + } + + while (readHeader()) { + entry.put(name, value); + } + + if (chunks != null) { + if (chunks.get(entryNameValue) != null) { + // TODO A bug: there might be several verification chunks for + // the same name. I believe they should be used to update + // signature in order of appearance; there are two ways to fix + // this: either use a list of chunks, or decide on used + // signature algorithm in advance and reread the chunks while + // updating the signature; for now a defensive error is thrown + throw new IOException(); + } + chunks.put(entryNameValue, new TManifest.Chunk(mark, pos)); + mark = pos; + } + + entries.put(entryNameValue, entry); + } + } + + int getPos() { + return pos; + } + + /** + * Number of subsequent line breaks. + */ + int linebreak; + + /** + * Read a single line from the manifest buffer. + */ + private boolean readHeader() throws IOException { + if (linebreak > 1) { + // break a section on an empty line + linebreak = 0; + return false; + } + readName(); + linebreak = 0; + readValue(); + // if the last line break is missed, the line + // is ignored by the reference implementation + return linebreak > 0; + } + + private byte[] wrap(int mark, int pos) { + byte[] buffer = new byte[pos - mark]; + System.arraycopy(buf, mark, buffer, 0, pos - mark); + return buffer; + } + + private void readName() throws IOException { + int i = 0; + int mark = pos; + + while (pos < buf.length) { + byte b = buf[pos++]; + + if (b == ':') { + byte[] nameBuffer = wrap(mark, pos - 1); + + if (buf[pos++] != ' ') { + throw new IOException(); + } + + name = new TAttributes.Name(nameBuffer); + return; + } + + if (!((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' + || b == '-' || (b >= '0' && b <= '9'))) { + throw new IOException(); + } + } + if (i > 0) { + throw new IOException(); + } + } + + private void readValue() throws IOException { + byte next; + boolean lastCr = false; + int mark = pos; + int last = pos; + + decoder.reset(); + cBuf.clear(); + + while (pos < buf.length) { + next = buf[pos++]; + + switch (next) { + case 0: + throw new IOException(); + case '\n': + if (lastCr) { + lastCr = false; + } else { + linebreak++; + } + continue; + case '\r': + lastCr = true; + linebreak++; + continue; + case ' ': + if (linebreak == 1) { + decode(mark, last, false); + mark = pos; + last = mark; + linebreak = 0; + continue; + } + } + + if (linebreak >= 1) { + pos--; + break; + } + last = pos; + } + + decode(mark, last, true); + while (CoderResult.OVERFLOW == decoder.flush(cBuf)) { + enlargeBuffer(); + } + value = new String(cBuf.array(), cBuf.arrayOffset(), cBuf.position()); + } + + private void decode(int mark, int pos, boolean endOfInput) + throws IOException { + ByteBuffer bBuf = ByteBuffer.wrap(buf, mark, pos - mark); + while (CoderResult.OVERFLOW == decoder.decode(bBuf, cBuf, endOfInput)) { + enlargeBuffer(); + } + } + + private void enlargeBuffer() { + CharBuffer newBuf = CharBuffer.allocate(cBuf.capacity() * 2); + newBuf.put(cBuf.array(), cBuf.arrayOffset(), cBuf.position()); + cBuf = newBuf; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarEntry.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarEntry.java new file mode 100644 index 000000000..93b475650 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarEntry.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.IOException; +import org.teavm.classlib.java.util.zip.TZipEntry; + +public class TJarEntry extends TZipEntry { + private TAttributes attributes; + TJarFile parentJar; + + private boolean isFactoryChecked; + + public TJarEntry(String name) { + super(name); + } + + public TJarEntry(TZipEntry entry) { + super(entry); + } + + public TAttributes getAttributes() throws IOException { + if (attributes != null || parentJar == null) { + return attributes; + } + TManifest manifest = parentJar.getManifest(); + if (manifest == null) { + return null; + } + attributes = manifest.getAttributes(getName()); + return attributes; + } + + void setAttributes(TAttributes attrib) { + attributes = attrib; + } + + public TJarEntry(TJarEntry je) { + super(je); + parentJar = je.parentJar; + attributes = je.attributes; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarException.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarException.java new file mode 100644 index 000000000..1db2c2393 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import org.teavm.classlib.java.util.zip.TZipException; + +public class TJarException extends TZipException { + public TJarException() { + super(); + } + + public TJarException(String detailMessage) { + super(detailMessage); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarFile.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarFile.java new file mode 100644 index 000000000..75363f603 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarFile.java @@ -0,0 +1,250 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.File; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import org.teavm.classlib.java.util.zip.TZipEntry; +import org.teavm.classlib.java.util.zip.TZipFile; + +public class TJarFile extends TZipFile { + public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + static final String META_DIR = "META-INF/"; + private TManifest manifest; + private TZipEntry manifestEntry; + private boolean closed; + + static final class JarFileInputStream extends FilterInputStream { + private long count; + private TZipEntry zipEntry; + + private boolean done; + + JarFileInputStream(InputStream is, TZipEntry ze) { + super(is); + zipEntry = ze; + count = zipEntry.getSize(); + } + + @Override + public int read() throws IOException { + if (done) { + return -1; + } + if (count > 0) { + int r = super.read(); + if (r != -1) { + count--; + } else { + count = 0; + } + if (count == 0) { + done = true; + } + return r; + } else { + done = true; + return -1; + } + } + + @Override + public int read(byte[] buf, int off, int nbytes) throws IOException { + if (done) { + return -1; + } + if (count > 0) { + int r = super.read(buf, off, nbytes); + if (r != -1) { + int size = r; + if (count < size) { + size = (int) count; + } + count -= size; + } else { + count = 0; + } + if (count == 0) { + done = true; + } + return r; + } else { + done = true; + return -1; + } + } + + @Override + public int available() throws IOException { + if (done) { + return 0; + } + return super.available(); + } + } + + public TJarFile(File file) throws IOException { + this(file, true); + } + + public TJarFile(File file, boolean verify) throws IOException { + super(file); + readMetaEntries(); + } + + public TJarFile(File file, boolean verify, int mode) throws IOException { + super(file, mode); + readMetaEntries(); + } + + public TJarFile(String filename) throws IOException { + this(filename, true); + } + + public TJarFile(String filename, boolean verify) throws IOException { + super(filename); + readMetaEntries(); + } + + @Override + public Enumeration entries() { + class JarFileEnumerator implements Enumeration { + Enumeration ze; + + TJarFile jf; + + JarFileEnumerator(Enumeration zenum, TJarFile jf) { + ze = zenum; + this.jf = jf; + } + + @Override + public boolean hasMoreElements() { + return ze.hasMoreElements(); + } + + @Override + public TJarEntry nextElement() { + TJarEntry je = new TJarEntry(ze.nextElement()); + je.parentJar = jf; + return je; + } + } + return new JarFileEnumerator(super.entries(), this); + } + + public TJarEntry getJarEntry(String name) { + return (TJarEntry) getEntry(name); + } + + public TManifest getManifest() throws IOException { + if (closed) { + throw new IllegalStateException("JarFile has been closed"); + } + if (manifest != null) { + return manifest; + } + try { + InputStream is = super.getInputStream(manifestEntry); + try { + manifest = new TManifest(is, false); + } finally { + is.close(); + } + manifestEntry = null; // Can discard the entry now. + } catch (NullPointerException e) { + manifestEntry = null; + } + return manifest; + } + + private void readMetaEntries() throws IOException { + // Get all meta directory entries + TZipEntry[] metaEntries = getMetaEntriesImpl(); + if (metaEntries == null) { + return; + } + + boolean signed = false; + + for (TZipEntry entry : metaEntries) { + String entryName = entry.getName(); + // Is this the entry for META-INF/MANIFEST.MF ? + if (manifestEntry == null && MANIFEST_NAME.equalsIgnoreCase(entryName)) { + manifestEntry = entry; + break; + } + } + } + + /** + * Return an {@code InputStream} for reading the decompressed contents of + * ZIP entry. + * + * @param ze + * the ZIP entry to be read. + * @return the input stream to read from. + * @throws IOException + * if an error occurred while creating the input stream. + */ + @Override + public InputStream getInputStream(TZipEntry ze) throws IOException { + if (manifestEntry != null) { + getManifest(); + } + return super.getInputStream(ze); + } + + @Override + public TZipEntry getEntry(String name) { + TZipEntry ze = super.getEntry(name); + if (ze == null) { + return ze; + } + TJarEntry je = new TJarEntry(ze); + je.parentJar = this; + return je; + } + + private TZipEntry[] getMetaEntriesImpl() { + List list = new ArrayList<>(8); + Enumeration allEntries = entries(); + while (allEntries.hasMoreElements()) { + TZipEntry ze = allEntries.nextElement(); + if (ze.getName().startsWith(META_DIR) && ze.getName().length() > META_DIR.length()) { + list.add(ze); + } + } + if (list.size() == 0) { + return null; + } + TZipEntry[] result = new TZipEntry[list.size()]; + list.toArray(result); + return result; + } + + @Override + public void close() throws IOException { + super.close(); + closed = true; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarInputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarInputStream.java new file mode 100644 index 000000000..1e1ea8a8b --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarInputStream.java @@ -0,0 +1,96 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.IOException; +import java.io.InputStream; +import org.teavm.classlib.java.util.zip.TZipEntry; +import org.teavm.classlib.java.util.zip.TZipInputStream; + +public class TJarInputStream extends TZipInputStream { + private TManifest manifest; + private TJarEntry mEntry; + private TJarEntry jarEntry; + + public TJarInputStream(InputStream stream, boolean verify) throws IOException { + super(stream); + mEntry = getNextJarEntry(); + if (mEntry == null) { + return; + } + String name = mEntry.getName().toUpperCase(); + if (name.equals(TJarFile.META_DIR)) { + mEntry = null; // modifies behavior of getNextJarEntry() + closeEntry(); + mEntry = getNextJarEntry(); + name = mEntry.getName().toUpperCase(); + } + if (name.equals(TJarFile.MANIFEST_NAME)) { + mEntry = null; + manifest = new TManifest(this, verify); + closeEntry(); + } else { + TAttributes temp = new TAttributes(3); + temp.map.put("hidden", null); + mEntry.setAttributes(temp); + } + } + + public TJarInputStream(InputStream stream) throws IOException { + this(stream, true); + } + + public TManifest getManifest() { + return manifest; + } + + public TJarEntry getNextJarEntry() throws IOException { + return (TJarEntry) getNextEntry(); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (mEntry != null) { + return -1; + } + return super.read(buffer, offset, length); + } + + @Override + public TZipEntry getNextEntry() throws IOException { + if (mEntry != null) { + jarEntry = mEntry; + mEntry = null; + jarEntry.setAttributes(null); + } else { + jarEntry = (TJarEntry) super.getNextEntry(); + if (jarEntry == null) { + return null; + } + } + return jarEntry; + } + + @Override + protected TZipEntry createZipEntry(String name) { + TJarEntry entry = new TJarEntry(name); + if (manifest != null) { + entry.setAttributes(manifest.getAttributes(name)); + } + return entry; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarOutputStream.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarOutputStream.java new file mode 100644 index 000000000..34d2e1151 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarOutputStream.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.IOException; +import java.io.OutputStream; +import org.teavm.classlib.java.util.zip.TZipEntry; +import org.teavm.classlib.java.util.zip.TZipOutputStream; + +public class TJarOutputStream extends TZipOutputStream { + private TManifest manifest; + + public TJarOutputStream(OutputStream os, TManifest mf) throws IOException { + super(os); + if (mf == null) { + throw new NullPointerException(); + } + manifest = mf; + TZipEntry ze = new TZipEntry(TJarFile.MANIFEST_NAME); + putNextEntry(ze); + manifest.write(this); + closeEntry(); + } + + public TJarOutputStream(OutputStream os) throws IOException { + super(os); + } + + @Override + public void putNextEntry(TZipEntry ze) throws IOException { + super.putNextEntry(ze); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarUtils.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarUtils.java new file mode 100644 index 000000000..91a859dbe --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TJarUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ +package org.teavm.classlib.java.util.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; + +final class TJarUtils { + private TJarUtils() { + } + + static boolean asciiEqualsIgnoreCase(byte[] a, byte[] b) { + if (a.length != b.length) { + return false; + } + + for (int i = 0; i < a.length; ++i) { + if (Character.toLowerCase((char) a[i]) != Character.toLowerCase((char) b[i])) { + return false; + } + } + + return true; + } + + static byte[] readFullyAndClose(InputStream input) throws IOException { + ByteBuffer result = ByteBuffer.wrap(new byte[Math.min(512, input.available())]); + while (true) { + if (result.remaining() == 0) { + result = ByteBuffer.wrap(Arrays.copyOf(result.array(), result.capacity() * 2)); + } + int actuallyRead = input.read(result.array(), result.position(), result.remaining()); + if (actuallyRead == -1) { + break; + } + result.position(result.position() + actuallyRead); + } + + byte[] b = Arrays.copyOf(result.array(), result.position()); + input.close(); + return b; + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/jar/TManifest.java b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TManifest.java new file mode 100644 index 000000000..ecf4efffe --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/util/jar/TManifest.java @@ -0,0 +1,253 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * Licensed 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. + */ + +package org.teavm.classlib.java.util.jar; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.HashMap; +import java.util.Map; + +public class TManifest implements Cloneable { + static final int LINE_LENGTH_LIMIT = 72; + private static final byte[] LINE_SEPARATOR = new byte[] { '\r', '\n' }; + private static final byte[] VALUE_SEPARATOR = new byte[] { ':', ' ' }; + private static final TAttributes.Name NAME_ATTRIBUTE = new TAttributes.Name("Name"); + private TAttributes mainAttributes = new TAttributes(); + private HashMap entries = new HashMap<>(); + + static class Chunk { + int start; + int end; + + Chunk(int start, int end) { + this.start = start; + this.end = end; + } + } + + private HashMap chunks; + private TInitManifest im; + private int mainEnd; + + public TManifest() { + super(); + } + + public TManifest(InputStream is) throws IOException { + super(); + read(is); + } + + @SuppressWarnings("unchecked") + public TManifest(TManifest man) { + mainAttributes = (TAttributes) man.mainAttributes.clone(); + entries = (HashMap) ((HashMap) man.getEntries()).clone(); + } + + TManifest(InputStream is, boolean readChunks) throws IOException { + if (readChunks) { + chunks = new HashMap<>(); + } + read(is); + } + + public void clear() { + im = null; + entries.clear(); + mainAttributes.clear(); + } + + public TAttributes getAttributes(String name) { + return getEntries().get(name); + } + + public Map getEntries() { + return entries; + } + + public TAttributes getMainAttributes() { + return mainAttributes; + } + + @Override + public Object clone() { + return new TManifest(this); + } + + public void write(OutputStream os) throws IOException { + write(this, os); + } + + public void read(InputStream is) throws IOException { + byte[] buf = readFully(is); + + if (buf.length == 0) { + return; + } + + // a workaround for HARMONY-5662 + // replace EOF and NUL with another new line + // which does not trigger an error + byte b = buf[buf.length - 1]; + if (0 == b || 26 == b) { + buf[buf.length - 1] = '\n'; + } + + // Attributes.Name.MANIFEST_VERSION is not used for + // the second parameter for RI compatibility + im = new TInitManifest(buf, mainAttributes, null); + mainEnd = im.getPos(); + // FIXME + im.initEntries(entries, chunks); + im = null; + } + + private byte[] readFully(InputStream is) throws IOException { + // Initial read + byte[] buffer = new byte[4096]; + int count = is.read(buffer); + int nextByte = is.read(); + + // Did we get it all in one read? + if (nextByte == -1) { + byte[] dest = new byte[count]; + System.arraycopy(buffer, 0, dest, 0, count); + return dest; + } + + // Does it look like a manifest? + if (!containsLine(buffer, count)) { + throw new IOException("Manifest is too long"); + } + + // Requires additional reads + ByteArrayOutputStream baos = new ByteArrayOutputStream(count * 2); + baos.write(buffer, 0, count); + baos.write(nextByte); + while (true) { + count = is.read(buffer); + if (count == -1) { + return baos.toByteArray(); + } + baos.write(buffer, 0, count); + } + } + + private boolean containsLine(byte[] buffer, int length) { + for (int i = 0; i < length; i++) { + if (buffer[i] == 0x0A || buffer[i] == 0x0D) { + return true; + } + } + return false; + } + + @Override + public int hashCode() { + return mainAttributes.hashCode() ^ getEntries().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (o.getClass() != this.getClass()) { + return false; + } + if (!mainAttributes.equals(((TManifest) o).mainAttributes)) { + return false; + } + return getEntries().equals(((TManifest) o).getEntries()); + } + + Chunk getChunk(String name) { + return chunks.get(name); + } + + void removeChunks() { + chunks = null; + } + + int getMainAttributesEnd() { + return mainEnd; + } + + static void write(TManifest manifest, OutputStream out) throws IOException { + CharsetEncoder encoder = Charset.defaultCharset().newEncoder(); + ByteBuffer buffer = ByteBuffer.allocate(512); + + String version = manifest.mainAttributes.getValue(TAttributes.Name.MANIFEST_VERSION); + if (version != null) { + writeEntry(out, TAttributes.Name.MANIFEST_VERSION, version, encoder, buffer); + for (Object o : manifest.mainAttributes.keySet()) { + TAttributes.Name name = (TAttributes.Name) o; + if (!name.equals(TAttributes.Name.MANIFEST_VERSION)) { + writeEntry(out, name, manifest.mainAttributes.getValue(name), encoder, buffer); + } + } + } + out.write(LINE_SEPARATOR); + for (String key : manifest.getEntries().keySet()) { + writeEntry(out, NAME_ATTRIBUTE, key, encoder, buffer); + TAttributes attrib = manifest.entries.get(key); + for (Object o : attrib.keySet()) { + TAttributes.Name name = (TAttributes.Name) o; + writeEntry(out, name, attrib.getValue(name), encoder, buffer); + } + out.write(LINE_SEPARATOR); + } + } + + private static void writeEntry(OutputStream os, TAttributes.Name name, + String value, CharsetEncoder encoder, ByteBuffer bBuf) throws IOException { + byte[] out = name.getBytes(); + if (out.length > LINE_LENGTH_LIMIT) { + throw new IOException(); + } + + os.write(out); + os.write(VALUE_SEPARATOR); + + encoder.reset(); + bBuf.clear().limit(LINE_LENGTH_LIMIT - out.length - 2); + + CharBuffer cBuf = CharBuffer.wrap(value); + CoderResult r; + + while (true) { + r = encoder.encode(cBuf, bBuf, true); + if (CoderResult.UNDERFLOW == r) { + r = encoder.flush(bBuf); + } + os.write(bBuf.array(), bBuf.arrayOffset(), bBuf.position()); + os.write(LINE_SEPARATOR); + if (CoderResult.UNDERFLOW == r) { + break; + } + os.write(' '); + bBuf.clear().limit(LINE_LENGTH_LIMIT - 1); + } + } +}