Initial tests for protobuf lite
Bug: 134709054, 112437944
Change-Id: Id03026156cc364f5174c12479c43913d8275968e
diff --git a/.gitignore b/.gitignore
index 191dbbd..e5f3042 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,6 +73,10 @@
!third_party/proguard/*.sha1
third_party/proguardsettings.tar.gz
third_party/proguardsettings/
+third_party/proto.tar.gz
+third_party/proto/
+third_party/protobuf-lite.tar.gz
+third_party/protobuf-lite/
third_party/youtube/*
!third_party/youtube/*sha1
third_party/jctf
diff --git a/build.gradle b/build.gradle
index 52abc60..7a31aee 100644
--- a/build.gradle
+++ b/build.gradle
@@ -151,6 +151,14 @@
}
output.resourcesDir = 'build/classes/examplesAndroidP'
}
+ examplesProto {
+ java {
+ srcDirs = ['src/test/examplesProto']
+ }
+ compileClasspath = files(file("build/generated/test/proto").listFiles())
+ compileClasspath += files("third_party/protobuf-lite/libprotobuf_lite.jar")
+ output.resourcesDir = 'build/classes/examplesProto'
+ }
jctfCommon {
java {
srcDirs = [
@@ -382,6 +390,8 @@
"photos/2017-06-06",
"proguard/proguard_internal_159423826",
"proguardsettings",
+ "proto",
+ "protobuf-lite",
"youtube/youtube.android_12.10",
"youtube/youtube.android_12.17",
"youtube/youtube.android_12.22",
@@ -1074,6 +1084,55 @@
}
}
+task buildProtoGeneratedSources {
+ def examplesProtoDir = file("src/test/examplesProto")
+ examplesProtoDir.eachDir { dir ->
+ def name = dir.getName()
+ task "compile_proto_generated_source_${name}"(type: JavaCompile) {
+ source = {
+ file('third_party/proto').listFiles()
+ .findAll { it.name.startsWith(name) && it.name.endsWith('-src.jar') }
+ .collect { zipTree(it) }
+ }
+ destinationDir = file("build/generated/test/proto/${name}_classes")
+ classpath = files("third_party/protobuf-lite/libprotobuf_lite.jar")
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ task "jar_proto_generated_source_${name}"(type: Jar, dependsOn: "compile_proto_generated_source_${name}") {
+ archiveName = "${name}.jar"
+ destinationDir = file("build/generated/test/proto")
+ from "build/generated/test/proto/${name}_classes"
+ include "/**/*.class"
+ }
+ dependsOn "jar_proto_generated_source_${name}"
+ }
+}
+
+task buildExamplesProto {
+ def examplesProtoDir = file("src/test/examplesProto")
+ def examplesProtoOutputDir = file("build/test/examplesProto");
+ dependsOn buildProtoGeneratedSources
+ task "compile_examples_proto"(type: JavaCompile) {
+ source = fileTree(dir: examplesProtoDir, include: "**/*.java")
+ destinationDir = file("build/test/examplesProto/classes")
+ classpath = files(file("build/generated/test/proto").listFiles())
+ classpath += files("third_party/protobuf-lite/libprotobuf_lite.jar")
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ examplesProtoDir.eachDir { dir ->
+ def name = dir.getName()
+ task "jar_examples_proto_${name}"(type: Jar, dependsOn: "compile_examples_proto") {
+ archiveName = "${name}.jar"
+ destinationDir = examplesProtoOutputDir
+ from "build/test/examplesProto/classes"
+ include name + "/**/*.class"
+ }
+ dependsOn "jar_examples_proto_${name}"
+ }
+}
+
// Proto lite generated code yields warnings when compiling with javac.
// We change the options passed to javac to ignore it.
compileExamplesJava.options.compilerArgs = ["-Xlint:none"]
@@ -1690,6 +1749,8 @@
}
if (project.hasProperty('no_internal')) {
exclude "com/android/tools/r8/internal/**"
+ } else {
+ dependsOn buildExamplesProto
}
if (project.hasProperty('only_internal')) {
include "com/android/tools/r8/internal/**"
diff --git a/src/test/examplesProto/proto2/TestClass.java b/src/test/examplesProto/proto2/TestClass.java
new file mode 100644
index 0000000..64c3ba7
--- /dev/null
+++ b/src/test/examplesProto/proto2/TestClass.java
@@ -0,0 +1,195 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package proto2;
+
+import com.android.tools.r8.proto2.Graph.IsExtendedWithOptional;
+import com.android.tools.r8.proto2.Graph.IsExtendedWithRequiredField;
+import com.android.tools.r8.proto2.Graph.IsRepeatedlyExtendedWithRequiredField;
+import com.android.tools.r8.proto2.Graph.UsedRoot;
+import com.android.tools.r8.proto2.Shrinking.ContainsFlaggedOffField;
+import com.android.tools.r8.proto2.Shrinking.HasFlaggedOffExtension;
+import com.android.tools.r8.proto2.Shrinking.PartiallyUsed;
+import com.android.tools.r8.proto2.Shrinking.PartiallyUsedWithExtension;
+import com.android.tools.r8.proto2.Shrinking.UsedViaHazzer;
+import com.android.tools.r8.proto2.Shrinking.UsedViaOneofCase;
+import com.android.tools.r8.proto2.Shrinking.UsesOnlyRepeatedFields;
+import com.android.tools.r8.proto2.TestProto.Primitives;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.WireFormat;
+import java.nio.ByteBuffer;
+
+public class TestClass {
+
+ public static void main(String[] args) {
+ roundtrip();
+ partiallyUsed_proto2();
+ usedViaHazzer();
+ usedViaOneofCase();
+ usesOnlyRepeatedFields();
+ containsFlaggedOffField();
+ hasFlaggedOffExtension();
+ useOneExtension();
+ keepMapAndRequiredFields();
+ }
+
+ // A protobuf payload indicating that varint field 1 is set to 42.
+ // See https://developers.google.com/protocol-buffers/docs/encoding
+ //
+ // Since serialization and deserialization use the same schema (which we're modifying), testing
+ // against wire-format data is preferred.
+ private static final byte[] FIELD1_SET_TO_42 =
+ new byte[] {(1 << 3) | WireFormat.WIRETYPE_VARINT, 42};
+
+ // A protobuf payload indicating that field 10 is a message whose field 1 is set to 42.
+ private static final byte[] MESSAGE10_WITH_FIELD1_SET_TO_42 =
+ ByteBuffer.allocate(4)
+ .put(
+ new byte[] {
+ (10 << 3) | WireFormat.WIRETYPE_LENGTH_DELIMITED, (byte) FIELD1_SET_TO_42.length
+ })
+ .put(FIELD1_SET_TO_42)
+ .array();
+
+ // smoke test
+ static void roundtrip() {
+ System.out.println("--- roundtrip ---");
+ Primitives primitives =
+ Primitives.newBuilder()
+ .setFooInt32(123)
+ .setOneofString("asdf")
+ .setBarInt64(Long.MAX_VALUE)
+ .setQuxString("qwerty")
+ .build();
+
+ Primitives roundtripped;
+ try {
+ roundtripped = Primitives.parseFrom(primitives.toByteArray());
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+
+ System.out.println(roundtripped.equals(primitives));
+ System.out.println(roundtripped.getFooInt32());
+ System.out.println(roundtripped.getOneofString());
+ System.out.println(roundtripped.getBarInt64());
+ System.out.println(roundtripped.getQuxString());
+ }
+
+ static void partiallyUsed_proto2() {
+ System.out.println("--- partiallyUsed_proto2 ---");
+ PartiallyUsed pu;
+ try {
+ pu = PartiallyUsed.parseFrom(FIELD1_SET_TO_42);
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ System.out.println(pu.hasUsed());
+ System.out.println(pu.getUsed());
+ }
+
+ static void usedViaHazzer() {
+ System.out.println("--- usedViaHazzer ---");
+ UsedViaHazzer uvh;
+ try {
+ uvh = UsedViaHazzer.parseFrom(FIELD1_SET_TO_42);
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ System.out.println(uvh.hasUsed());
+ }
+
+ static void usedViaOneofCase() {
+ System.out.println("--- usedViaOneofCase ---");
+ UsedViaOneofCase msg;
+ try {
+ msg = UsedViaOneofCase.parseFrom(FIELD1_SET_TO_42);
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ System.out.println(msg.hasUsed());
+ }
+
+ static void usesOnlyRepeatedFields() {
+ System.out.println("--- usesOnlyRepeatedFields ---");
+ UsesOnlyRepeatedFields msg;
+ try {
+ msg = UsesOnlyRepeatedFields.parseFrom(FIELD1_SET_TO_42);
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ System.out.println(msg.getUsedCount());
+ }
+
+ static void containsFlaggedOffField() {
+ System.out.println("--- containsFlaggedOffField ---");
+ ContainsFlaggedOffField.Builder builder = ContainsFlaggedOffField.newBuilder();
+ if (alwaysFalse()) {
+ builder.setConditionallyUsed(1);
+ }
+ System.out.println(builder.build().getSerializedSize());
+ }
+
+ static void hasFlaggedOffExtension() {
+ System.out.println("--- hasFlaggedOffExtension ---");
+ HasFlaggedOffExtension msg;
+ try {
+ msg =
+ HasFlaggedOffExtension.parseFrom(
+ MESSAGE10_WITH_FIELD1_SET_TO_42, ExtensionRegistryLite.getGeneratedRegistry());
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ if (alwaysFalse()) {
+ System.out.println(msg.getExtension(HasFlaggedOffExtension.Ext.ext).getX());
+ }
+ System.out.println(msg.getSerializedSize());
+ }
+
+ static boolean alwaysFalse() {
+ return false;
+ }
+
+ static void useOneExtension() {
+ System.out.println("--- useOneExtension ---");
+
+ PartiallyUsedWithExtension msg;
+ try {
+ msg =
+ PartiallyUsedWithExtension.parseFrom(
+ MESSAGE10_WITH_FIELD1_SET_TO_42, ExtensionRegistryLite.getGeneratedRegistry());
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+
+ PartiallyUsedWithExtension.ExtA ext = msg.getExtension(PartiallyUsedWithExtension.ExtA.extA);
+ System.out.println(ext.getX());
+ }
+
+ static void keepMapAndRequiredFields() {
+ System.out.println("--- keepMapAndRequiredFields ---");
+
+ UsedRoot msg;
+ try {
+ msg = UsedRoot.parseFrom(new byte[0], ExtensionRegistryLite.getGeneratedRegistry());
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ System.out.println(msg.isInitialized());
+ // Force extension edges to be kept. This test is for verifying that we keep *fields* which
+ // lead to extensions with required fields. b/123031088 is for keeping the extensions.
+ System.out.println(IsExtendedWithRequiredField.Ext.ext.getNumber());
+ System.out.println(IsRepeatedlyExtendedWithRequiredField.Ext.ext.getNumber());
+ System.out.println(IsExtendedWithOptional.Ext.ext.getNumber());
+ }
+}
diff --git a/src/test/examplesProto/proto3/TestClass.java b/src/test/examplesProto/proto3/TestClass.java
new file mode 100644
index 0000000..1eec5d8
--- /dev/null
+++ b/src/test/examplesProto/proto3/TestClass.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package proto3;
+
+import com.android.tools.r8.proto3.Shrinking.PartiallyUsed;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.WireFormat;
+
+public class TestClass {
+
+ public static void main(String[] args) {
+ partiallyUsed_proto3();
+ }
+
+ // A protobuf payload indicating that varint field 1 is set to 42.
+ // See https://developers.google.com/protocol-buffers/docs/encoding
+ //
+ // Since serialization and deserialization use the same schema (which we're modifying), testing
+ // against wire-format data is preferred.
+ private static final byte[] FIELD1_SET_TO_42 =
+ new byte[] {(1 << 3) | WireFormat.WIRETYPE_VARINT, 42};
+
+ static void partiallyUsed_proto3() {
+ System.out.println("--- partiallyUsed_proto3 ---");
+ PartiallyUsed pu;
+ try {
+ pu = PartiallyUsed.parseFrom(FIELD1_SET_TO_42);
+ } catch (InvalidProtocolBufferException e) {
+ System.out.println("Unexpected exception: " + e);
+ throw new RuntimeException(e);
+ }
+ System.out.println(pu.getUsed());
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 1d9fd27..9349161 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -79,7 +79,9 @@
public class ToolHelper {
public static final String BUILD_DIR = "build/";
+ public static final String GENERATED_TEST_BUILD_DIR = BUILD_DIR + "generated/test/";
public static final String LIBS_DIR = BUILD_DIR + "libs/";
+ public static final String THIRD_PARTY_DIR = "third_party/";
public static final String TOOLS_DIR = "tools/";
public static final String TESTS_DIR = "src/test/";
public static final String EXAMPLES_DIR = TESTS_DIR + "examples/";
@@ -99,6 +101,8 @@
public static final String EXAMPLES_JAVA9_BUILD_DIR = TESTS_BUILD_DIR + "examplesJava9/";
public static final String EXAMPLES_JAVA11_JAR_DIR = TESTS_BUILD_DIR + "examplesJava11/";
public static final String EXAMPLES_JAVA11_BUILD_DIR = BUILD_DIR + "classes/java/examplesJava11/";
+ public static final String EXAMPLES_PROTO_BUILD_DIR = TESTS_BUILD_DIR + "examplesProto/";
+ public static final String GENERATED_PROTO_BUILD_DIR = GENERATED_TEST_BUILD_DIR + "proto/";
public static final String SMALI_DIR = TESTS_DIR + "smali/";
public static final String SMALI_BUILD_DIR = TESTS_BUILD_DIR + "smali/";
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
new file mode 100644
index 0000000..485d90a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto2ShrinkingTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.internal.proto;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class Proto2ShrinkingTest extends ProtoShrinkingTestBase {
+
+ private final boolean allowAccessModification;
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{1}, allow access modification: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+ }
+
+ public Proto2ShrinkingTest(boolean allowAccessModification, TestParameters parameters) {
+ this.allowAccessModification = allowAccessModification;
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramFiles(PROTO2_EXAMPLES_JAR, PROTO2_PROTO_JAR, PROTOBUF_LITE_JAR)
+ .addKeepMainRule("proto2.TestClass")
+ .addKeepRules(
+ // TODO(b/112437944): Fix -identifiernamestring support.
+ "-keepnames class * extends com.google.protobuf.GeneratedMessageLite",
+ // TODO(b/112437944): Use dex item based const strings for proto schema definitions.
+ "-keepclassmembernames class * extends com.google.protobuf.GeneratedMessageLite {",
+ " <fields>;",
+ "}",
+ // TODO(b/112437944): Do not remove proto fields that are actually used in tree shaking.
+ "-keepclassmembers,allowobfuscation class * extends",
+ " com.google.protobuf.GeneratedMessageLite {",
+ " <fields>;",
+ "}",
+ allowAccessModification ? "-allowaccessmodification" : "")
+ .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES)
+ .setMinApi(parameters.getRuntime())
+ .compile()
+ .run(parameters.getRuntime(), "proto2.TestClass")
+ .assertSuccessWithOutputLines(
+ "--- roundtrip ---",
+ "true",
+ "123",
+ "asdf",
+ "9223372036854775807",
+ "qwerty",
+ "--- partiallyUsed_proto2 ---",
+ "true",
+ "42",
+ "--- usedViaHazzer ---",
+ "true",
+ "--- usedViaOneofCase ---",
+ "true",
+ "--- usesOnlyRepeatedFields ---",
+ "1",
+ "--- containsFlaggedOffField ---",
+ "0",
+ "--- hasFlaggedOffExtension ---",
+ "4",
+ "--- useOneExtension ---",
+ "42",
+ "--- keepMapAndRequiredFields ---",
+ "true",
+ "10",
+ "10",
+ "10");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
new file mode 100644
index 0000000..e536067
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/proto/Proto3ShrinkingTest.java
@@ -0,0 +1,52 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.internal.proto;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class Proto3ShrinkingTest extends ProtoShrinkingTestBase {
+
+ private final boolean allowAccessModification;
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{1}, allow access modification: {0}")
+ public static List<Object[]> data() {
+ return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+ }
+
+ public Proto3ShrinkingTest(boolean allowAccessModification, TestParameters parameters) {
+ this.allowAccessModification = allowAccessModification;
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void test() throws Exception {
+ testForR8(parameters.getBackend())
+ .addProgramFiles(PROTO3_EXAMPLES_JAR, PROTO3_PROTO_JAR, PROTOBUF_LITE_JAR)
+ .addKeepMainRule("proto3.TestClass")
+ .addKeepRules(
+ // TODO(b/112437944): Use dex item based const strings for proto schema definitions.
+ "-keepclassmembernames class * extends com.google.protobuf.GeneratedMessageLite {",
+ " <fields>;",
+ "}",
+ // TODO(b/112437944): Do not remove proto fields that are actually used in tree shaking.
+ "-keepclassmembers,allowobfuscation class * extends",
+ " com.google.protobuf.GeneratedMessageLite {",
+ " <fields>;",
+ "}",
+ allowAccessModification ? "-allowaccessmodification" : "")
+ .addKeepRuleFiles(PROTOBUF_LITE_PROGUARD_RULES)
+ .setMinApi(parameters.getRuntime())
+ .compile()
+ .run(parameters.getRuntime(), "proto3.TestClass")
+ .assertSuccessWithOutputLines("--- partiallyUsed_proto3 ---", "42");
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
new file mode 100644
index 0000000..43ceaff
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/proto/ProtoShrinkingTestBase.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.internal.proto;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public abstract class ProtoShrinkingTestBase extends TestBase {
+
+ public static final Path PROTOBUF_LITE_JAR =
+ Paths.get("third_party/protobuf-lite/libprotobuf_lite.jar");
+
+ public static final Path PROTOBUF_LITE_PROGUARD_RULES =
+ Paths.get("third_party/protobuf-lite/lite_proguard.pgcfg");
+
+ // Test classes for proto2.
+ public static final Path PROTO2_EXAMPLES_JAR =
+ Paths.get(ToolHelper.EXAMPLES_PROTO_BUILD_DIR).resolve("proto2.jar");
+
+ // Proto definitions used by test classes for proto2.
+ public static final Path PROTO2_PROTO_JAR =
+ Paths.get(ToolHelper.GENERATED_PROTO_BUILD_DIR).resolve("proto2.jar");
+
+ // Test classes for proto3.
+ public static final Path PROTO3_EXAMPLES_JAR =
+ Paths.get(ToolHelper.EXAMPLES_PROTO_BUILD_DIR).resolve("proto3.jar");
+
+ // Proto definitions used by test classes for proto3.
+ public static final Path PROTO3_PROTO_JAR =
+ Paths.get(ToolHelper.GENERATED_PROTO_BUILD_DIR).resolve("proto3.jar");
+}
diff --git a/third_party/proto.tar.gz.sha1 b/third_party/proto.tar.gz.sha1
new file mode 100644
index 0000000..dae5c12
--- /dev/null
+++ b/third_party/proto.tar.gz.sha1
@@ -0,0 +1 @@
+18151ef2484c03b5d1f8fc0084202cb9482f664d
\ No newline at end of file
diff --git a/third_party/protobuf-lite.tar.gz.sha1 b/third_party/protobuf-lite.tar.gz.sha1
new file mode 100644
index 0000000..6c757bb
--- /dev/null
+++ b/third_party/protobuf-lite.tar.gz.sha1
@@ -0,0 +1 @@
+f5f3295899f7eecd937830fc8a0a35a4ef5b4083
\ No newline at end of file