|
21 | 21 | */ |
22 | 22 | package pro.javacard.sdk; |
23 | 23 |
|
| 24 | +import pro.javacard.capfile.HexUtils; |
| 25 | + |
24 | 26 | import java.io.DataInputStream; |
25 | 27 | import java.io.IOException; |
| 28 | +import java.io.InputStream; |
26 | 29 | import java.nio.file.Files; |
27 | 30 | import java.nio.file.Path; |
28 | | -import java.util.Optional; |
| 31 | +import java.util.HashMap; |
| 32 | +import java.util.Map; |
29 | 33 |
|
| 34 | +// Export file format: JCVM Spec v3.2, Chapter 5 "The Export File Format" |
30 | 35 | public final class ExportFileHelper { |
31 | 36 |
|
| 37 | + // JCVM 5.5: "The magic item contains the magic number identifying the ExportFile format; it has the value 0x00FACADE." |
| 38 | + static final int MAGIC = 0x00FACADE; |
| 39 | + |
| 40 | + // JCVM 5.6, Table 5-1: Export File Constant Pool Tags |
| 41 | + static final int TAG_UTF8 = 1; |
| 42 | + static final int TAG_INTEGER = 3; |
| 43 | + static final int TAG_CLASSREF = 7; |
| 44 | + static final int TAG_PACKAGE = 13; |
| 45 | + |
32 | 46 | public enum ExportFileVersion { |
33 | | - V21, V23 |
| 47 | + V21, |
| 48 | + V22, |
| 49 | + V23 |
| 50 | + } |
| 51 | + |
| 52 | + public static final class PackageInfo { |
| 53 | + private final ExportFileVersion version; |
| 54 | + private final String name; |
| 55 | + private final byte[] aid; |
| 56 | + private final int major; |
| 57 | + private final int minor; |
| 58 | + private final boolean library; |
| 59 | + |
| 60 | + PackageInfo(ExportFileVersion version, String name, byte[] aid, |
| 61 | + int major, int minor, boolean library) { |
| 62 | + this.version = version; |
| 63 | + this.name = name; |
| 64 | + this.aid = aid.clone(); |
| 65 | + this.major = major; |
| 66 | + this.minor = minor; |
| 67 | + this.library = library; |
| 68 | + } |
| 69 | + |
| 70 | + public ExportFileVersion getVersion() { |
| 71 | + return version; |
| 72 | + } |
| 73 | + |
| 74 | + public String getName() { |
| 75 | + return name; |
| 76 | + } |
| 77 | + |
| 78 | + public byte[] getAid() { |
| 79 | + return aid.clone(); |
| 80 | + } |
| 81 | + |
| 82 | + public int getMajor() { |
| 83 | + return major; |
| 84 | + } |
| 85 | + |
| 86 | + public int getMinor() { |
| 87 | + return minor; |
| 88 | + } |
| 89 | + |
| 90 | + public String getPackageVersion() { |
| 91 | + return String.format("%d.%d", major, minor); |
| 92 | + } |
| 93 | + |
| 94 | + public boolean isLibrary() { |
| 95 | + return library; |
| 96 | + } |
| 97 | + |
| 98 | + @Override |
| 99 | + public String toString() { |
| 100 | + return String.format("%s %s%s v%s (%s)", name, HexUtils.bin2hex(aid), library ? " library" : "", getPackageVersion(), version); |
| 101 | + } |
34 | 102 | } |
35 | 103 |
|
36 | 104 | private ExportFileHelper() { |
37 | 105 | } |
38 | 106 |
|
39 | | - public static Optional<ExportFileVersion> getVersion(Path path) throws IOException { |
40 | | - try (DataInputStream dis = new DataInputStream(Files.newInputStream(path))) { |
41 | | - int magic = dis.readInt(); |
42 | | - |
43 | | - byte minor = dis.readByte(); |
44 | | - byte major = dis.readByte(); |
45 | | - |
46 | | - if (magic != 0x00FACADE) |
47 | | - return Optional.empty(); |
48 | | - if (major != 2) |
49 | | - throw new IOException("Invalid major version: " + major); |
50 | | - if (minor == 1) |
51 | | - return Optional.of(ExportFileVersion.V21); |
52 | | - if (minor == 3) |
53 | | - return Optional.of(ExportFileVersion.V23); |
54 | | - throw new IOException("Invalid minor version: " + minor); |
| 107 | + public static PackageInfo parsePackage(Path path) throws IOException { |
| 108 | + try (InputStream in = Files.newInputStream(path)) { |
| 109 | + return parsePackage(in); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + public static PackageInfo parsePackage(InputStream in) throws IOException { |
| 114 | + DataInputStream dis = new DataInputStream(in); |
| 115 | + |
| 116 | + // JCVM 5.5: magic (u4) |
| 117 | + int magic = dis.readInt(); |
| 118 | + if (magic != MAGIC) { |
| 119 | + throw new IllegalArgumentException(String.format("Bad magic: 0x%08X", magic)); |
| 120 | + } |
| 121 | + |
| 122 | + // JCVM 5.5: minor_version (u1), major_version (u1) |
| 123 | + byte fileMinor = dis.readByte(); |
| 124 | + byte fileMajor = dis.readByte(); |
| 125 | + if (fileMajor != 2) { |
| 126 | + throw new IllegalArgumentException("Invalid export file major version: " + fileMajor); |
| 127 | + } |
| 128 | + ExportFileVersion version = parseFileVersion(fileMinor); |
| 129 | + |
| 130 | + // JCVM 5.5: constant_pool_count (u2) |
| 131 | + int cpCount = dis.readUnsignedShort(); |
| 132 | + // JCVM 5.6: constant_pool[] |
| 133 | + Object[] pool = new Object[cpCount]; |
| 134 | + |
| 135 | + // Store all CONSTANT_Package entries by index, since this_package |
| 136 | + // tells us which one is the actual exported package |
| 137 | + Map<Integer, int[]> pkgEntries = new HashMap<>(); // index -> [flags, nameIndex, minor, major] |
| 138 | + Map<Integer, byte[]> pkgAids = new HashMap<>(); // index -> aid |
| 139 | + |
| 140 | + for (int i = 0; i < cpCount; i++) { |
| 141 | + int tag = dis.readUnsignedByte(); |
| 142 | + switch (tag) { |
| 143 | + case TAG_UTF8: { |
| 144 | + // JCVM 5.6.4: length (u2), bytes[length] |
| 145 | + int len = dis.readUnsignedShort(); |
| 146 | + byte[] bytes = new byte[len]; |
| 147 | + dis.readFully(bytes); |
| 148 | + pool[i] = new String(bytes, "UTF-8"); |
| 149 | + break; |
| 150 | + } |
| 151 | + case TAG_INTEGER: { |
| 152 | + // JCVM 5.6.3: bytes (u4) |
| 153 | + dis.readInt(); |
| 154 | + break; |
| 155 | + } |
| 156 | + case TAG_CLASSREF: { |
| 157 | + // JCVM 5.6.2: name_index (u2) |
| 158 | + dis.readUnsignedShort(); |
| 159 | + break; |
| 160 | + } |
| 161 | + case TAG_PACKAGE: { |
| 162 | + // JCVM 5.6.1: flags (u1), name_index (u2), |
| 163 | + // minor_version (u1), major_version (u1), aid_length (u1), aid[aid_length] |
| 164 | + int flags = dis.readUnsignedByte(); |
| 165 | + int nameIndex = dis.readUnsignedShort(); |
| 166 | + int minor = dis.readUnsignedByte(); |
| 167 | + int major = dis.readUnsignedByte(); |
| 168 | + int aidLen = dis.readUnsignedByte(); |
| 169 | + byte[] aid = new byte[aidLen]; |
| 170 | + dis.readFully(aid); |
| 171 | + pkgEntries.put(i, new int[]{flags, nameIndex, minor, major}); |
| 172 | + pkgAids.put(i, aid); |
| 173 | + break; |
| 174 | + } |
| 175 | + default: |
| 176 | + throw new IllegalArgumentException(String.format("Unknown constant pool tag: %d at index %d", tag, i)); |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + // JCVM 5.5: this_package (u2) - index into constant pool identifying the exported package |
| 181 | + int thisPackage = dis.readUnsignedShort(); |
| 182 | + |
| 183 | + if (!pkgEntries.containsKey(thisPackage)) { |
| 184 | + throw new IllegalArgumentException(String.format("this_package index %d does not point to a CONSTANT_Package", thisPackage)); |
| 185 | + } |
| 186 | + |
| 187 | + int[] pkg = pkgEntries.get(thisPackage); |
| 188 | + int pkgFlags = pkg[0]; |
| 189 | + int pkgNameIndex = pkg[1]; |
| 190 | + int pkgMinor = pkg[2]; |
| 191 | + int pkgMajor = pkg[3]; |
| 192 | + byte[] pkgAid = pkgAids.get(thisPackage); |
| 193 | + |
| 194 | + if (pkgNameIndex >= cpCount || !(pool[pkgNameIndex] instanceof String)) { |
| 195 | + throw new IllegalArgumentException("Invalid package name index: " + pkgNameIndex); |
| 196 | + } |
| 197 | + |
| 198 | + // JCVM 5.6.1: name_index -> CONSTANT_Utf8 with fully qualified package name using '/' |
| 199 | + String name = ((String) pool[pkgNameIndex]).replace('/', '.'); |
| 200 | + |
| 201 | + // JCVM 5.6.1, Table 5-2: "If bit 0 of the flags item is set, this package is a library" |
| 202 | + boolean library = (pkgFlags & 0x01) != 0; |
| 203 | + |
| 204 | + return new PackageInfo(version, name, pkgAid, pkgMajor, pkgMinor, library); |
| 205 | + } |
| 206 | + |
| 207 | + // JCVM 5.5: "major version has the value 2", minor 1=v2.1, 2=v2.2, 3=v2.3 |
| 208 | + private static ExportFileVersion parseFileVersion(int minor) { |
| 209 | + switch (minor) { |
| 210 | + case 1: |
| 211 | + return ExportFileVersion.V21; |
| 212 | + case 2: |
| 213 | + return ExportFileVersion.V22; |
| 214 | + case 3: |
| 215 | + return ExportFileVersion.V23; |
| 216 | + default: |
| 217 | + throw new IllegalArgumentException("Invalid export file minor version: " + minor); |
55 | 218 | } |
56 | 219 | } |
57 | 220 | } |
0 commit comments