mirror of
https://github.com/revanced/Apktool.git
synced 2024-12-05 02:22:55 +01:00
fix: redesign StyledString decoding (#2816)
* fix: redesign StyledString decoding * optimize: avoid calling span.getName() twice * fix: order spans due to aapt1/2 discrepancy * fixes: #2815
This commit is contained in:
parent
0997fb98b7
commit
c1f6dc792a
@ -19,22 +19,14 @@ package brut.androlib.res.decoder;
|
|||||||
import brut.androlib.res.xml.ResXmlEncoders;
|
import brut.androlib.res.xml.ResXmlEncoders;
|
||||||
import brut.util.ExtDataInput;
|
import brut.util.ExtDataInput;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Splitter;
|
|
||||||
import com.google.common.base.Splitter.MapSplitter;
|
|
||||||
import com.google.common.collect.ComparisonChain;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.*;
|
import java.nio.charset.*;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.StringJoiner;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static com.google.common.collect.Ordering.explicit;
|
|
||||||
import static java.util.Comparator.naturalOrder;
|
|
||||||
import static java.util.Comparator.reverseOrder;
|
|
||||||
|
|
||||||
public class StringBlock {
|
public class StringBlock {
|
||||||
|
|
||||||
@ -109,183 +101,34 @@ public class StringBlock {
|
|||||||
return decodeString(offset, length);
|
return decodeString(offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Tag implements Comparable<Tag> {
|
|
||||||
private static final MapSplitter ATTRIBUTES_SPLITTER =
|
|
||||||
Splitter.on(';').withKeyValueSeparator(Splitter.on('=').limit(2));
|
|
||||||
|
|
||||||
private final String tag;
|
|
||||||
private final Type type;
|
|
||||||
private final int position;
|
|
||||||
private final int matchingTagPosition;
|
|
||||||
|
|
||||||
Tag(String tag, Type type, int position, int matchingTagPosition) {
|
|
||||||
this.tag = ResXmlEncoders.escapeXmlChars(tag);
|
|
||||||
this.type = type;
|
|
||||||
this.position = position;
|
|
||||||
this.matchingTagPosition = matchingTagPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* compares this tag and another, returning the order that should be between them.
|
|
||||||
* order by:
|
|
||||||
* position
|
|
||||||
* closing tag has precedence over openning tag (unless it is the same tag)
|
|
||||||
* tags that are enclosed in others should appear later if openning tag, or first if closing tag
|
|
||||||
* lexicographical sort. openning tag and closing tag in reverse so that one tag will be contained in the other and not each contain the other partially
|
|
||||||
* @param o - the other tag object to compare to
|
|
||||||
* @return the order in between this object and the other
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int compareTo(Tag o) {
|
|
||||||
return ComparisonChain.start()
|
|
||||||
.compare(position, o.position)
|
|
||||||
// When one tag closes where another starts, we always close before opening.
|
|
||||||
.compare(type, o.type, this.tag.equals(o.tag) ? explicit(Type.OPEN, Type.CLOSE) : explicit(Type.CLOSE, Type.OPEN))
|
|
||||||
// Open first the tag which closes last, and close first the tag which opened last.
|
|
||||||
.compare(matchingTagPosition, o.matchingTagPosition, reverseOrder())
|
|
||||||
// When two tags open and close together, we order alphabetically. When they close,
|
|
||||||
// we reversed the order. This ensures that the XML tags are properly nested.
|
|
||||||
.compare(tag, o.tag, type.equals(Type.OPEN) ? naturalOrder() : reverseOrder())
|
|
||||||
.result();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* formats the tag value and attributes according to whether the tag is an openning or closing tag
|
|
||||||
* @return the formatted tag value as a string
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
// "tag" can either be just the tag or have the form "tag;attr1=value1;attr2=value2;[...]".
|
|
||||||
int separatorIdx = tag.indexOf(';');
|
|
||||||
String actualTag = separatorIdx == -1 ? tag : tag.substring(0, separatorIdx);
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case OPEN:
|
|
||||||
if (separatorIdx != -1) {
|
|
||||||
StringJoiner attributes = new StringJoiner(" ");
|
|
||||||
ATTRIBUTES_SPLITTER
|
|
||||||
.split(tag.substring(separatorIdx + 1, tag.endsWith(";") ? tag.length() - 1: tag.length()))
|
|
||||||
.forEach((key, value) -> attributes.add(String.format("%s=\"%s\"", key, value)));
|
|
||||||
return String.format("<%s %s>", actualTag, attributes);
|
|
||||||
}
|
|
||||||
return String.format("<%s>", actualTag);
|
|
||||||
case CLOSE:
|
|
||||||
return String.format("</%s>", actualTag);
|
|
||||||
}
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum Type {
|
|
||||||
OPEN,
|
|
||||||
CLOSE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Span {
|
|
||||||
private String tag;
|
|
||||||
private int firstChar, lastChar;
|
|
||||||
|
|
||||||
Span(String val, int firstIndex, int lastIndex) {
|
|
||||||
this.tag = val;
|
|
||||||
this.firstChar = firstIndex;
|
|
||||||
this.lastChar = lastIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTag() {
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getFirstChar() {
|
|
||||||
return firstChar;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getLastChar() {
|
|
||||||
return lastChar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class StyledString {
|
|
||||||
String val;
|
|
||||||
int[] styles;
|
|
||||||
|
|
||||||
StyledString(String raw, int[] stylesArr) {
|
|
||||||
this.val = raw;
|
|
||||||
this.styles = stylesArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getValue() {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Span> getSpanList(StringBlock stringBlock) {
|
|
||||||
ArrayList<Span> spanList = new ArrayList<>();
|
|
||||||
for (int i = 0; i != styles.length; i += 3) {
|
|
||||||
spanList.add(new Span(stringBlock.getString(styles[i]), styles[i + 1], styles[i + 2]));
|
|
||||||
}
|
|
||||||
return spanList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param styledString - the raw string with its corresponding styling tags and their locations
|
|
||||||
* @return a formatted styled string that contains the styling tag in the correct locations
|
|
||||||
*/
|
|
||||||
String processStyledString(StyledString styledString) {
|
|
||||||
|
|
||||||
ArrayList<Tag> sortedTagsList = new ArrayList<>();
|
|
||||||
|
|
||||||
styledString.getSpanList(this).stream()
|
|
||||||
.flatMap(
|
|
||||||
span ->
|
|
||||||
Stream.of(
|
|
||||||
// "+ 1" because the last char is included.
|
|
||||||
new Tag(
|
|
||||||
span.getTag(), Tag.Type.OPEN, span.getFirstChar(), span.getLastChar() + 1),
|
|
||||||
// "+ 1" because the last char is included.
|
|
||||||
new Tag(
|
|
||||||
span.getTag(),
|
|
||||||
Tag.Type.CLOSE,
|
|
||||||
span.getLastChar() + 1,
|
|
||||||
span.getFirstChar())))
|
|
||||||
// So we can edit the string in place, we need to start from the end.
|
|
||||||
.sorted(naturalOrder())
|
|
||||||
.forEach(tag -> sortedTagsList.add(tag));
|
|
||||||
|
|
||||||
String raw = styledString.getValue();
|
|
||||||
StringBuilder string = new StringBuilder(raw.length() + 32);
|
|
||||||
int lastIndex = 0;
|
|
||||||
for (Tag tag : sortedTagsList) {
|
|
||||||
string.append(ResXmlEncoders.escapeXmlChars(raw.substring(lastIndex, tag.position)));
|
|
||||||
string.append(tag);
|
|
||||||
lastIndex = tag.position;
|
|
||||||
}
|
|
||||||
string.append(ResXmlEncoders.escapeXmlChars(raw.substring(lastIndex)));
|
|
||||||
|
|
||||||
return string.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param index Location (index) of string to process to HTML
|
* @param index Location (index) of string to process to HTML
|
||||||
* @return String Returns string with style tags (html-like).
|
* @return String Returns string with style tags (html-like).
|
||||||
*/
|
*/
|
||||||
public String getHTML(int index) {
|
public String getHTML(int index) {
|
||||||
String raw = getString(index);
|
String text = getString(index);
|
||||||
if (raw == null) {
|
if (text == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int[] style = getStyle(index);
|
int[] style = getStyle(index);
|
||||||
if (style == null) {
|
if (style == null) {
|
||||||
return ResXmlEncoders.escapeXmlChars(raw);
|
return ResXmlEncoders.escapeXmlChars(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the returned style is further in string, than string length. Lets skip it.
|
// If the returned style is further in string, than string length. Lets skip it.
|
||||||
if (style[1] > raw.length()) {
|
if (style[1] > text.length()) {
|
||||||
return ResXmlEncoders.escapeXmlChars(raw);
|
return ResXmlEncoders.escapeXmlChars(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledString styledString = new StyledString(raw, style);
|
// Convert styles to spans
|
||||||
return processStyledString(styledString);
|
List<StyledString.Span> spans = new ArrayList<>(style.length / 3);
|
||||||
|
for (int i = 0; i < style.length; i += 3) {
|
||||||
|
spans.add(new StyledString.Span(getString(style[i]), style[i + 1], style[i + 2]));
|
||||||
|
}
|
||||||
|
Collections.sort(spans);
|
||||||
|
|
||||||
|
StyledString styledString = new StyledString(text, spans);
|
||||||
|
return styledString.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
|
||||||
|
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* https://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 brut.androlib.res.decoder;
|
||||||
|
|
||||||
|
import brut.androlib.res.xml.ResXmlEncoders;
|
||||||
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.base.Splitter.MapSplitter;
|
||||||
|
import com.google.common.collect.Iterators;
|
||||||
|
import com.google.common.collect.PeekingIterator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class StyledString {
|
||||||
|
private final String mText;
|
||||||
|
private final List<Span> mSpans;
|
||||||
|
|
||||||
|
public StyledString(String text, List<Span> spans) {
|
||||||
|
this.mText = text;
|
||||||
|
this.mSpans = spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getText() {
|
||||||
|
return mText;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Span> getSpans() {
|
||||||
|
return mSpans;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return new Decoder().decode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Span implements Comparable<Span> {
|
||||||
|
private static final MapSplitter ATTRIBUTES_SPLITTER =
|
||||||
|
Splitter.on(';').withKeyValueSeparator(Splitter.on('=').limit(2));
|
||||||
|
|
||||||
|
private final String tag;
|
||||||
|
private final int firstChar;
|
||||||
|
private final int lastChar;
|
||||||
|
|
||||||
|
public Span(String tag, int firstChar, int lastChar) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.firstChar = firstChar;
|
||||||
|
this.lastChar = lastChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFirstChar() {
|
||||||
|
return firstChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLastChar() {
|
||||||
|
return lastChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
int separatorIdx = tag.indexOf(';');
|
||||||
|
return separatorIdx == -1 ? tag : tag.substring(0, separatorIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getAttributes() {
|
||||||
|
int separatorIdx = tag.indexOf(';');
|
||||||
|
return separatorIdx == -1 ? null : ATTRIBUTES_SPLITTER.split(
|
||||||
|
tag.substring(separatorIdx + 1, tag.endsWith(";") ? tag.length() - 1 : tag.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Span o) {
|
||||||
|
int res = Integer.compare(firstChar, o.firstChar);
|
||||||
|
if (res != 0) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
res = Integer.compare(lastChar, o.lastChar);
|
||||||
|
if (res != 0) {
|
||||||
|
return -res;
|
||||||
|
}
|
||||||
|
return -tag.compareTo(o.tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Decoder {
|
||||||
|
private String text;
|
||||||
|
private StringBuilder xmlValue;
|
||||||
|
private int lastOffset;
|
||||||
|
|
||||||
|
String decode(StyledString styledString) {
|
||||||
|
text = styledString.getText();
|
||||||
|
xmlValue = new StringBuilder(text.length() * 2);
|
||||||
|
lastOffset = 0;
|
||||||
|
|
||||||
|
// recurse top-level tags
|
||||||
|
PeekingIterator<Span> it = Iterators.peekingIterator(styledString.getSpans().iterator());
|
||||||
|
while (it.hasNext()) {
|
||||||
|
decodeIterate(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the remaining encoded raw text
|
||||||
|
if (lastOffset < text.length()) {
|
||||||
|
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset)));
|
||||||
|
}
|
||||||
|
return xmlValue.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decodeIterate(PeekingIterator<Span> it) {
|
||||||
|
Span span = it.next();
|
||||||
|
String name = span.getName();
|
||||||
|
Map<String, String> attributes = span.getAttributes();
|
||||||
|
int spanStart = span.getFirstChar();
|
||||||
|
int spanEnd = span.getLastChar() + 1;
|
||||||
|
|
||||||
|
// write encoded raw text preceding the opening tag
|
||||||
|
if (spanStart > lastOffset) {
|
||||||
|
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset, spanStart)));
|
||||||
|
}
|
||||||
|
lastOffset = spanStart;
|
||||||
|
|
||||||
|
// write opening tag
|
||||||
|
xmlValue.append('<').append(name);
|
||||||
|
if (attributes != null) {
|
||||||
|
for (Map.Entry<String, String> attrEntry : attributes.entrySet()) {
|
||||||
|
xmlValue.append(' ').append(attrEntry.getKey()).append("=\"")
|
||||||
|
.append(ResXmlEncoders.escapeXmlChars(attrEntry.getValue())).append('"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if an opening tag is followed by a matching closing tag, write as an empty-element tag
|
||||||
|
if (spanStart == spanEnd) {
|
||||||
|
xmlValue.append("/>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xmlValue.append('>');
|
||||||
|
|
||||||
|
// recurse nested tags
|
||||||
|
while (it.hasNext() && it.peek().getFirstChar() < spanEnd) {
|
||||||
|
decodeIterate(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write encoded raw text preceding the closing tag
|
||||||
|
if (spanEnd > lastOffset) {
|
||||||
|
xmlValue.append(ResXmlEncoders.escapeXmlChars(text.substring(lastOffset, spanEnd)));
|
||||||
|
}
|
||||||
|
lastOffset = spanEnd;
|
||||||
|
|
||||||
|
// write closing tag
|
||||||
|
xmlValue.append("</").append(name).append('>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(StyledString.class.getName());
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user