-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds support for resources.arsc parsing (#22)
* Adds support for resources.arsc parsing * Addressing issues from review * Enhance the output of the xsd-validator tool (#19) Added ✅ PASSED / ❌ FAILED to the validator tool output * Create a new workflow for the current release (#24) * Create a new workflow for the release tagged as "release" The release tagged as "release" is meant to contain the artifacts that should be used in the validation pipeline at the current moment. It differs from "latest", which is built automatically and might contain changes that we do not want to be used for now. To create a new release, the new workflow must be triggered manually with the git sha of the commit from where we want to build the artifacts. The artifacts will be available on the following permalink: ``` https://github.com/{owner}/{repo}/releases/download/{release-name}/{artifact-name} ``` * Fix workflow config * Updates validator version list in memory tool * Clarified versions with comment * Updates XSD with latest fixes (#27) * Use the gradle wrapper version instead of a hardcoded one (#30) Modify the GitHub action config to use the gradle version used while developing, via the gradle wrapper, rather than setting a hardcoded version in the CI config, that is different than what is used while developing. * Add tests for the memory footprint tool (#29) Import the existing tests for the memory footprint tool. # Conflicts: # play-validations/memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/InputPackage.java * Count analog clock hands in active Include the analog hands when used with the AnalogClock element in the active calculation. * Updates obfuscated resource handling for tests * Updates obfuscated resource handling for tests * Adds DuplicateStrategy to copy * Updates duplicate strategy * Updates implementation to use streams * Updates implementation to use streams * Fixes formatting * Remove redundant code --------- Co-authored-by: amoledwatchfaces™ <[email protected]> Co-authored-by: Lucian Boaca <[email protected]> Co-authored-by: Lucian Boaca <[email protected]>
- Loading branch information
1 parent
f136a1a
commit e6849ed
Showing
12 changed files
with
459 additions
and
198 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
.../memory-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package com.google.wear.watchface.dfx.memory; | ||
|
||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* Represents a resource, from an AAB or APK. | ||
*/ | ||
public class AndroidResource { | ||
private static final Pattern VALID_RESOURCE_PATH = Pattern.compile(".*res/([^-/]+).*/([^.]+)(\\.|)(.*|)$"); | ||
private static final int VALID_RESOURCE_GROUPS = 4; | ||
|
||
// Resource type, for example "raw", "asset", "drawable" etc. | ||
private final String resourceType; | ||
|
||
// Resource name, for example "watchface" for res/raw/watchface.xml. | ||
private final String resourceName; | ||
|
||
// File extension of the resource, for example "xml" for res/raw/watchface.xml | ||
private final String extension; | ||
|
||
// Path in the package. This is the obfuscated path to the actual data, where obfuscation has | ||
// been used, for example "res/raw/watchface.xml" may point to something like "res/li.xml". | ||
private final Path filePath; | ||
|
||
// The resource data itself. | ||
private final byte[] data; | ||
|
||
public AndroidResource( | ||
String resourceType, | ||
String resourceName, | ||
String extension, | ||
Path filePath, | ||
byte[] data | ||
) { | ||
this.resourceType = resourceType; | ||
this.resourceName = resourceName; | ||
this.extension = extension; | ||
this.filePath = filePath; | ||
this.data = data; | ||
} | ||
|
||
public String getResourceName() { | ||
return resourceName; | ||
} | ||
|
||
public Path getFilePath() { | ||
return filePath; | ||
} | ||
|
||
public byte[] getData() { | ||
return data; | ||
} | ||
|
||
// TODO: This should be improved to parse res/xml/watch_face_info.xml where present, so as not | ||
// to assume all XML files in the res/raw directory are watch face XML files. | ||
public Boolean isWatchFaceXml() { | ||
return "xml".equals(extension) && "raw".equals(resourceType); | ||
} | ||
|
||
public Boolean isDrawable() { return "drawable".equals(resourceType); } | ||
|
||
public Boolean isFont() { return "font".equals(resourceType); } | ||
|
||
public Boolean isAsset() { return "asset".equals(resourceType); } | ||
|
||
public Boolean isRaw() { return "raw".equals(resourceType); } | ||
|
||
static AndroidResource fromPath(Path filePath, byte[] data) { | ||
String pathWithFwdSlashes = filePath.toString().replace('\\', '/'); | ||
Matcher m = VALID_RESOURCE_PATH.matcher(pathWithFwdSlashes); | ||
if (m.matches() && m.groupCount() == VALID_RESOURCE_GROUPS) { | ||
String resType = m.group(1); | ||
String resName = m.group(2); | ||
String ext = m.group(4); | ||
return new AndroidResource( | ||
resType, | ||
resName, | ||
ext, | ||
filePath, | ||
data | ||
); | ||
} | ||
throw new RuntimeException("Not a valid resource file: " + m.matches()); | ||
} | ||
|
||
static AndroidResource fromPath(String filePath, byte[] data) { | ||
return fromPath(Paths.get(filePath), data); | ||
} | ||
|
||
static Boolean isValidResourcePath(Path filePath) { | ||
Matcher m = VALID_RESOURCE_PATH.matcher(filePath.toString()); | ||
return m.matches() && m.groupCount() == VALID_RESOURCE_GROUPS; | ||
} | ||
|
||
static Boolean isValidResourcePath(String filePath) { | ||
return isValidResourcePath(Paths.get(filePath)); | ||
} | ||
} |
237 changes: 237 additions & 0 deletions
237
...y-footprint/src/main/java/com/google/wear/watchface/dfx/memory/AndroidResourceLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
package com.google.wear.watchface.dfx.memory; | ||
|
||
import com.google.common.io.Files; | ||
import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceFile; | ||
import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceValue; | ||
import com.google.devrel.gmscore.tools.apk.arsc.Chunk; | ||
import com.google.devrel.gmscore.tools.apk.arsc.ResourceTableChunk; | ||
import com.google.devrel.gmscore.tools.apk.arsc.StringPoolChunk; | ||
import com.google.devrel.gmscore.tools.apk.arsc.TypeChunk; | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import java.util.Spliterator; | ||
import java.util.Spliterators; | ||
import java.util.stream.Stream; | ||
import java.util.stream.StreamSupport; | ||
import java.util.zip.ZipEntry; | ||
import java.util.zip.ZipFile; | ||
import java.util.zip.ZipInputStream; | ||
|
||
/** | ||
* Represents all the resources of interest in the Android package. | ||
* <p> | ||
* Where obfuscation has been applied, the mapping is derived, for the creation of the | ||
* AndroidResource objects. For example, for a logical resource res/raw/watchface.xml, an APK | ||
* may in fact store this as res/aB.xml. The resources.arsc file contains this mapping, and the | ||
* AndroidResourceLoader provides a stream of resources with their logical types, names and data. | ||
* <p> | ||
* Note that more than one AndroidResource object can exist for the given dimensions. For example, | ||
* if there is a drawable and a drawable-fr folder, then there may be multiple AndroidResource | ||
* entries for drawables with the same type, name and extension. The configuration detail, e.g. | ||
* "fr" or "default", is not currently exposed in the AndroidResource objects as it isn't used. | ||
*/ | ||
public class AndroidResourceLoader { | ||
// Only certain resource types are of interest to the evaluator, notably, not string resources. | ||
private static final Set<String> RESOURCE_TYPES = Set.of("raw", "xml", "drawable", "font", "asset"); | ||
private static final String RESOURCES_FILE_NAME = "resources.arsc"; | ||
|
||
private AndroidResourceLoader() { } | ||
|
||
/** | ||
* Creates a resource stream from a path to an AAB structure on the file system. | ||
* | ||
* @param aabPath The path to the root of the AAB directory. | ||
* @return A stream of Android resource objects. | ||
* @throws IOException when the resources file cannot be found, or other IO errors occur. | ||
*/ | ||
static Stream<AndroidResource> streamFromAabDirectory(Path aabPath) throws IOException { | ||
Stream<Path> childrenFilesStream = java.nio.file.Files.walk(aabPath); | ||
int relativePathOffset = aabPath.toString().length() + 1; | ||
|
||
return childrenFilesStream | ||
.map( | ||
filePath -> { | ||
AndroidResource resource = null; | ||
if (AndroidResource.isValidResourcePath(filePath)) { | ||
try { | ||
resource = AndroidResource.fromPath( | ||
Paths.get(filePath.toString().substring(relativePathOffset)), | ||
java.nio.file.Files.readAllBytes(filePath) | ||
); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} else if (filePath.endsWith("manifest/AndroidManifest.xml")) { | ||
try { | ||
resource = new AndroidResource( | ||
"xml", | ||
"AndroidManifest", | ||
"xml", | ||
Paths.get(filePath.toString().substring(relativePathOffset)), | ||
java.nio.file.Files.readAllBytes(filePath) | ||
); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
return resource; | ||
} | ||
).filter(Objects::nonNull); | ||
} | ||
|
||
/** | ||
* Creates a stream of resource objects from the AAB file. | ||
* | ||
* @param aabZipFile The zip file object representing the AAB. | ||
* @return A stream of resource objects. | ||
*/ | ||
static Stream<AndroidResource> streamFromAabFile(ZipFile aabZipFile) { | ||
return aabZipFile.stream() | ||
.map( | ||
zipEntry -> { | ||
Path zipEntryPath = Paths.get(zipEntry.getName()); | ||
AndroidResource resource = null; | ||
if (AndroidResource.isValidResourcePath(zipEntryPath)) { | ||
try { | ||
resource = AndroidResource.fromPath( | ||
zipEntryPath, | ||
aabZipFile.getInputStream(zipEntry).readAllBytes() | ||
); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} else if (zipEntry.getName().endsWith("manifest/AndroidManifest.xml")) { | ||
try { | ||
resource = new AndroidResource( | ||
"xml", | ||
"AndroidManifest", | ||
"xml", | ||
Paths.get(zipEntry.getName()), | ||
aabZipFile.getInputStream(zipEntry).readAllBytes() | ||
); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
return resource; | ||
} | ||
).filter(Objects::nonNull); | ||
} | ||
|
||
/** | ||
* Creates a resource stream from a base split entry within an archive | ||
* | ||
* @param baseSplitZipStream The zip entry for the base split. | ||
* @return A stream of resource objects. | ||
* @throws IOException when the resources file cannot be found, or other IO errors occur. | ||
*/ | ||
static Stream<AndroidResource> streamFromMokkaZip(ZipInputStream baseSplitZipStream) throws IOException { | ||
|
||
Iterator<AndroidResource> iterator = | ||
new Iterator<AndroidResource>() { | ||
private ZipEntry zipEntry; | ||
|
||
@Override | ||
public boolean hasNext() { | ||
try { | ||
zipEntry = baseSplitZipStream.getNextEntry(); | ||
// Advance over entries in the zip that aren't relevant. | ||
while (zipEntry != null && !AndroidResource.isValidResourcePath(zipEntry.getName())) { | ||
zipEntry = baseSplitZipStream.getNextEntry(); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
return zipEntry != null; | ||
} | ||
|
||
@Override | ||
public AndroidResource next() { | ||
System.out.println(zipEntry); | ||
byte[] entryData; | ||
try { | ||
entryData = readAllBytes(baseSplitZipStream); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
return AndroidResource.fromPath(zipEntry.getName(), entryData); | ||
} | ||
}; | ||
|
||
return StreamSupport.stream( | ||
Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), | ||
false); | ||
} | ||
|
||
/** | ||
* Creates a resource stream from an APK zip file. | ||
* <p> | ||
* APK files can have their resources obfuscated, so it is necessary to extract the mapping | ||
* between the original path and the path in the obfuscated zip. | ||
* | ||
* @param zipFile The APK zip file | ||
* @return A stream of resource objects. | ||
* @throws IOException when errors loading resources occur. | ||
*/ | ||
static Stream<AndroidResource> streamFromApkFile(ZipFile zipFile) throws IOException { | ||
ZipEntry arscEntry = new ZipEntry(RESOURCES_FILE_NAME); | ||
InputStream is = zipFile.getInputStream(arscEntry); | ||
|
||
BinaryResourceFile resources = BinaryResourceFile.fromInputStream(is); | ||
List<Chunk> chunks = resources.getChunks(); | ||
if (chunks.isEmpty()) { | ||
throw new IOException("no chunks"); | ||
} | ||
if (!(chunks.get(0) instanceof ResourceTableChunk)) { | ||
throw new IOException("no res table chunk"); | ||
} | ||
ResourceTableChunk table = (ResourceTableChunk) chunks.get(0); | ||
StringPoolChunk stringPool = table.getStringPool(); | ||
|
||
List<TypeChunk> typeChunks = table.getPackages() | ||
.stream() | ||
.flatMap(p -> p.getTypeChunks().stream()) | ||
.toList(); | ||
|
||
return typeChunks.stream() | ||
.flatMap(c -> c.getEntries().values().stream()) | ||
.filter(t -> RESOURCE_TYPES.contains(t.typeName())) | ||
.filter(t -> t.value().type() == BinaryResourceValue.Type.STRING) | ||
.map(entry -> { | ||
Path path = Path.of(stringPool.getString(entry.value().data())); | ||
byte[] data = null; | ||
try { | ||
data = zipFile.getInputStream(new ZipEntry(path.toString())).readAllBytes(); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
||
return new AndroidResource( | ||
entry.parent().getTypeName(), | ||
entry.key(), | ||
Files.getFileExtension(path.toString()), | ||
path, | ||
data | ||
); | ||
}); | ||
} | ||
|
||
/** Read all bytes from an input stream to a new byte array. */ | ||
static byte[] readAllBytes(InputStream inputStream) throws IOException { | ||
int len; | ||
byte[] buffer = new byte[1024]; | ||
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | ||
while ((len = inputStream.read(buffer)) > 0) { | ||
bos.write(buffer, 0, len); | ||
} | ||
return bos.toByteArray(); | ||
} | ||
} |
Oops, something went wrong.