1 package edu.jiangxin.apktoolbox.convert.protobuf.supervised; 2 3 import com.google.protobuf.Descriptors; 4 import com.google.protobuf.DynamicMessage; 5 import com.google.protobuf.InvalidProtocolBufferException; 6 import com.google.protobuf.util.JsonFormat; 7 8 import java.io.ByteArrayInputStream; 9 import java.io.ByteArrayOutputStream; 10 import java.io.IOException; 11 import java.io.UncheckedIOException; 12 import java.nio.file.Files; 13 import java.nio.file.Path; 14 import java.util.Comparator; 15 import java.util.Objects; 16 import java.util.Optional; 17 import java.util.zip.InflaterInputStream; 18 19 /** 20 * API to decode binary protobuf messages to readable JSON, based on protobuf descriptors given as {@link 21 * DescriptorCache}. 22 * <p> 23 * Instances can be created using the static factory methods {@link #fromCache(DescriptorCache)} and {@link 24 * #fromEmptyCache()}. In order to add additional descriptors to the cache, it can be get using {@link #getCache()}. 25 * <p> 26 * Conversion can be done using one of the various {@code toJson} overloads, for example {@link #toJson(Path)}. Variants 27 * accepting a {@code String messageTypeName}, like {@link #toJson(Path, String)} will use a descriptor suitable for the 28 * given message type. Other variants will attempt to automatically find a best match among all available descriptors. 29 * <p> 30 * The conversion methods throw 31 * <ul> 32 * <li>{@link UncheckedIOException} if an I/O error occurs during conversion,</li> 33 * <li>{@link NoDescriptorFoundException} if no suitable descriptor could be found and</li> 34 * <li>{@link UncheckedInvalidProtocolBufferException} if a message could not be parsed with the demanded descriptor.</li> 35 * </ul> 36 * <p> 37 * Usage example: 38 * <pre>{@code 39 * DescriptorCache cache = DescriptorCache.fromDirectory(Path.of("descriptorCache")); 40 * ProtoToJson protoToJson = ProtoToJson.fromCache(cache); 41 * 42 * String json = protoToJson.toJson(Path.of("someProtoMessage.message")); 43 * }</pre> 44 * Descriptors can be obtained by applying a {@code protoc} command on the protobuf schema {@code .proto}, for example: 45 * <pre>{@code 46 * protoc --descriptor_set_out foo.desc foo.proto 47 * }</pre> 48 * 49 * @author Daniel Tischner {@literal <zabuza.dev@gmail.com>} 50 */ 51 public final class ProtoToJson { 52 /** 53 * Creates an API from a given descriptor cache. 54 * 55 * @param cache The cache to use, not null 56 * 57 * @return The API instance 58 */ 59 public static ProtoToJson fromCache(final DescriptorCache cache) { 60 Objects.requireNonNull(cache); 61 62 return new ProtoToJson(cache); 63 } 64 65 /** 66 * Creates an API from an empty descriptor cache. The cache can be get using {@link #getCache()}. 67 * 68 * @return The API instance 69 */ 70 public static ProtoToJson fromEmptyCache() { 71 return new ProtoToJson(DescriptorCache.emptyCache()); 72 } 73 74 /** 75 * Decompresses the given data using ZLib (see {@link InflaterInputStream}). 76 * 77 * @param compressed The data to decompress, not null 78 * 79 * @return The decompressed data 80 * 81 * @throws IOException If an I/O error occurs during the decompression or if the given data is not ZLib encoded 82 */ 83 private static byte[] decompressZLib(final byte[] compressed) throws IOException { 84 Objects.requireNonNull(compressed); 85 try (final ByteArrayInputStream inputStream = new ByteArrayInputStream(compressed); 86 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 87 final InflaterInputStream zLibOutputStream = new InflaterInputStream(inputStream)) { 88 zLibOutputStream.transferTo(outputStream); 89 return outputStream.toByteArray(); 90 } 91 } 92 93 /** 94 * The cache used by the API, not null. 95 */ 96 private final DescriptorCache cache; 97 /** 98 * The printer used to convert protobuf messages to JSOn, not null. 99 */ 100 private final JsonFormat.Printer printer = JsonFormat.printer(); 101 102 /** 103 * Creates a new instance that uses the given descriptor cache. 104 * 105 * @param cache The cache to use, not null 106 */ 107 private ProtoToJson(final DescriptorCache cache) { 108 this.cache = Objects.requireNonNull(cache); 109 } 110 111 /** 112 * Gets the descriptor cache used by the API. For example, to add more descriptors to the cache. 113 * 114 * @return The cache used by the API 115 */ 116 public DescriptorCache getCache() { 117 return cache; 118 } 119 120 /** 121 * Converts the protobuf message given as a file to JSON. 122 * <p> 123 * The method will attempt to find a suitable descriptor to parse the message from the cache. 124 * 125 * @param messageFile Path to the protobuf message file, not null 126 * 127 * @return The JSON decoded message 128 * 129 * @throws NoDescriptorFoundException If no suitable descriptor to parse the message could be found in 130 * the descriptor cache 131 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format 132 * expected by the descriptor 133 * @throws UncheckedIOException If an I/O error occurs during reading the file 134 */ 135 public String toJson(final Path messageFile) { 136 Objects.requireNonNull(messageFile); 137 138 try { 139 return toJson(Files.readAllBytes(messageFile), (String) null); 140 } catch (final IOException e) { 141 throw new UncheckedIOException(e); 142 } 143 } 144 145 /** 146 * Converts the given raw protobuf message to JSON. 147 * <p> 148 * The method will attempt to find a suitable descriptor to parse the message from the cache. 149 * 150 * @param messageRaw The raw protobuf message, not null 151 * 152 * @return The JSON decoded message 153 * 154 * @throws NoDescriptorFoundException If no suitable descriptor to parse the message could be found in 155 * the descriptor cache 156 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format 157 * expected by the descriptor 158 */ 159 public String toJson(final byte[] messageRaw) { 160 Objects.requireNonNull(messageRaw); 161 162 return toJson(messageRaw, (String) null); 163 } 164 165 /** 166 * Converts the protobuf message given as a file to JSON. 167 * <p> 168 * If {@code messageTypeName} is null, it will attempt to find a suitable descriptor from the cache. Otherwise it 169 * will use a descriptor for the given message type. 170 * 171 * @param messageFile Path to the protobuf message file, not null 172 * @param messageTypeName The name of the messages type or null if the API should pick a suitable descriptor itself 173 * 174 * @return The JSON decoded message 175 * 176 * @throws NoDescriptorFoundException If no suitable descriptor to parse the message could be found in 177 * the descriptor cache 178 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format 179 * expected by the descriptor 180 * @throws UncheckedIOException If an I/O error occurs during reading the file 181 */ 182 public String toJson(final Path messageFile, final String messageTypeName) { 183 Objects.requireNonNull(messageFile); 184 185 try { 186 return toJson(Files.readAllBytes(messageFile), messageTypeName); 187 } catch (final IOException e) { 188 throw new UncheckedIOException(e); 189 } 190 } 191 192 /** 193 * Converts the given raw protobuf message to JSON. 194 * <p> 195 * If {@code messageTypeName} is null, it will attempt to find a suitable descriptor from the cache. Otherwise it 196 * will use a descriptor for the given message type. 197 * 198 * @param messageRaw The raw protobuf message, not null 199 * @param messageTypeName The name of the messages type or null if the API should pick a suitable descriptor itself 200 * 201 * @return The JSON decoded message 202 * 203 * @throws NoDescriptorFoundException If no suitable descriptor to parse the message could be found in 204 * the descriptor cache 205 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format 206 * expected by the descriptor 207 */ 208 @SuppressWarnings("WeakerAccess") 209 public String toJson(final byte[] messageRaw, final String messageTypeName) { 210 Objects.requireNonNull(messageRaw); 211 212 if (messageTypeName == null) { 213 final Optional<DynamicMessage> message = parseWithBestMatchingDescriptor(messageRaw); 214 return toJson(message.orElseThrow(NoDescriptorFoundException::new)); 215 } 216 217 final Optional<Descriptors.Descriptor> descriptor = cache.getByTypeName(messageTypeName); 218 return toJson(messageRaw, descriptor.orElseThrow(() -> new NoDescriptorFoundException(messageTypeName))); 219 } 220 221 /** 222 * Converts the given raw protobuf message to JSON using the given descriptor. 223 * 224 * @param messageRaw The raw protobuf message, not null 225 * @param descriptor The descriptor to use for parsing the message, not null 226 * 227 * @return The JSON decoded message 228 * 229 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format or not in the format 230 * expected by the given descriptor 231 */ 232 @SuppressWarnings("WeakerAccess") 233 public String toJson(final byte[] messageRaw, final Descriptors.Descriptor descriptor) { 234 Objects.requireNonNull(messageRaw); 235 Objects.requireNonNull(descriptor); 236 237 try { 238 return toJson(DynamicMessage.parseFrom(descriptor, messageRaw)); 239 } catch (final InvalidProtocolBufferException e) { 240 throw new UncheckedInvalidProtocolBufferException(e); 241 } 242 } 243 244 /** 245 * Converts the given protobuf message to JSON. 246 * 247 * @param message The protobuf message, not null 248 * 249 * @return The JSON decoded message 250 * 251 * @throws UncheckedInvalidProtocolBufferException If the message is in an invalid format 252 */ 253 @SuppressWarnings("WeakerAccess") 254 public String toJson(final DynamicMessage message) { 255 Objects.requireNonNull(message); 256 257 try { 258 return printer.print(message); 259 } catch (final InvalidProtocolBufferException e) { 260 throw new UncheckedInvalidProtocolBufferException(e); 261 } 262 } 263 264 /** 265 * Attempts to parse the given binary protobuf message with a suitable descriptor from the APIs descriptor cache. 266 * 267 * @param messageRaw The binary protobuf message to parse, not null 268 * 269 * @return The parsed protobuf message, if a suitable descriptor was found, otherwise empty 270 */ 271 private Optional<DynamicMessage> parseWithBestMatchingDescriptor(final byte[] messageRaw) { 272 Objects.requireNonNull(messageRaw); 273 274 return cache.getDescriptors() 275 .stream() 276 .map(descriptor -> { 277 try { 278 return DynamicMessage.parseFrom(descriptor, messageRaw); 279 } catch (final InvalidProtocolBufferException e) { 280 //noinspection ReturnOfNull 281 return null; 282 } 283 }) 284 .filter(Objects::nonNull) 285 .min(Comparator.comparingInt(message -> message.getUnknownFields() 286 .asMap() 287 .size())); 288 } 289 }