| // Copyright 2023 The Bazel Authors. All rights reserved. |
| // |
| // 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 |
| // |
| // http://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 com.google.devtools.build.lib.bazel.bzlmod; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.gson.Gson; |
| import com.google.gson.GsonBuilder; |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonElement; |
| import com.google.gson.JsonNull; |
| import com.google.gson.JsonObject; |
| import com.google.gson.JsonParser; |
| import com.google.gson.JsonPrimitive; |
| import com.google.gson.TypeAdapter; |
| import com.google.gson.stream.JsonReader; |
| import com.google.gson.stream.JsonWriter; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import net.starlark.java.eval.Dict; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Mutability; |
| import net.starlark.java.eval.Starlark; |
| import net.starlark.java.eval.StarlarkInt; |
| import net.starlark.java.eval.StarlarkList; |
| |
| /** Helps serialize/deserialize {@link AttributeValues}, which contains Starlark values. */ |
| public class AttributeValuesAdapter extends TypeAdapter<AttributeValues> { |
| |
| private final Gson gson = new GsonBuilder().disableHtmlEscaping().create(); |
| |
| @Override |
| public void write(JsonWriter out, AttributeValues attributeValues) throws IOException { |
| out.beginObject(); |
| for (Map.Entry<String, Object> entry : attributeValues.attributes().entrySet()) { |
| out.name(entry.getKey()); |
| gson.toJson(serializeObject(entry.getValue()), out); |
| } |
| out.endObject(); |
| } |
| |
| @Override |
| public AttributeValues read(JsonReader in) throws IOException { |
| JsonObject jsonObject = JsonParser.parseReader(in).getAsJsonObject(); |
| Dict.Builder<String, Object> dict = Dict.builder(); |
| for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) { |
| dict.put(entry.getKey(), deserializeObject(entry.getValue())); |
| } |
| return AttributeValues.create(dict.buildImmutable()); |
| } |
| |
| /** |
| * Starlark Object Types Bool Integer String Label List (Int, label, string) Dict (String,list) & |
| * (Label, String) |
| */ |
| private JsonElement serializeObject(Object obj) { |
| if (obj.equals(Starlark.NONE)) { |
| return JsonNull.INSTANCE; |
| } else if (obj instanceof Boolean) { |
| return new JsonPrimitive((Boolean) obj); |
| } else if (obj instanceof StarlarkInt) { |
| try { |
| return new JsonPrimitive(((StarlarkInt) obj).toInt("serialization into the lockfile")); |
| } catch (EvalException e) { |
| throw new IllegalArgumentException("Unable to parse StarlarkInt to Integer: " + e); |
| } |
| } else if (obj instanceof String || obj instanceof Label) { |
| return new JsonPrimitive(serializeObjToString(obj)); |
| } else if (obj instanceof Dict) { |
| JsonObject jsonObject = new JsonObject(); |
| for (Map.Entry<?, ?> entry : ((Dict<?, ?>) obj).entrySet()) { |
| jsonObject.add(serializeObjToString(entry.getKey()), serializeObject(entry.getValue())); |
| } |
| return jsonObject; |
| } else if (obj instanceof StarlarkList) { |
| JsonArray jsonArray = new JsonArray(); |
| for (Object item : (StarlarkList<?>) obj) { |
| jsonArray.add(serializeObject(item)); |
| } |
| return jsonArray; |
| } else { |
| throw new IllegalArgumentException("Unsupported type: " + obj.getClass()); |
| } |
| } |
| |
| private Object deserializeObject(JsonElement json) { |
| if (json == null || json.isJsonNull()) { |
| return Starlark.NONE; |
| } else if (json.isJsonPrimitive()) { |
| JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive(); |
| if (jsonPrimitive.isBoolean()) { |
| return jsonPrimitive.getAsBoolean(); |
| } else if (jsonPrimitive.isNumber()) { |
| return StarlarkInt.of(jsonPrimitive.getAsInt()); |
| } else if (jsonPrimitive.isString()) { |
| return deserializeStringToObject(jsonPrimitive.getAsString()); |
| } else { |
| throw new IllegalArgumentException("Unsupported JSON primitive: " + jsonPrimitive); |
| } |
| } else if (json.isJsonObject()) { |
| JsonObject jsonObject = json.getAsJsonObject(); |
| Dict.Builder<Object, Object> dict = Dict.builder(); |
| for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) { |
| dict.put(deserializeStringToObject(entry.getKey()), deserializeObject(entry.getValue())); |
| } |
| return dict.buildImmutable(); |
| } else if (json.isJsonArray()) { |
| JsonArray jsonArray = json.getAsJsonArray(); |
| List<Object> list = new ArrayList<>(); |
| for (JsonElement item : jsonArray) { |
| list.add(deserializeObject(item)); |
| } |
| return StarlarkList.copyOf(Mutability.IMMUTABLE, list); |
| } else { |
| throw new IllegalArgumentException("Unsupported JSON element: " + json); |
| } |
| } |
| |
| @VisibleForTesting static final String STRING_ESCAPE_SEQUENCE = "'"; |
| |
| /** |
| * Serializes an object (Label or String) to String. A label is converted to a String as it is. A |
| * String that looks like a label is escaped so that it can be differentiated from a label when |
| * deserializing, otherwise it is emitted as is. |
| * |
| * @param obj String or Label |
| * @return serialized object |
| */ |
| private String serializeObjToString(Object obj) { |
| if (obj instanceof Label) { |
| String labelString = ((Label) obj).getUnambiguousCanonicalForm(); |
| Preconditions.checkState(labelString.startsWith("@@")); |
| return labelString; |
| } |
| String string = (String) obj; |
| // Strings that start with "@@" need to be escaped to avoid being interpreted as a label. We |
| // escape by wrapping the string in the escape sequence and strip one layer of this sequence |
| // during deserialization, so strings that happen to already start and end with the escape |
| // sequence also have to be escaped. |
| if (string.startsWith("@@") |
| || (string.startsWith(STRING_ESCAPE_SEQUENCE) && string.endsWith(STRING_ESCAPE_SEQUENCE))) { |
| return STRING_ESCAPE_SEQUENCE + string + STRING_ESCAPE_SEQUENCE; |
| } |
| return string; |
| } |
| |
| /** |
| * Deserializes a string to either a label or a String depending on the prefix and presence of the |
| * escape sequence. |
| * |
| * @param value String to be deserialized |
| * @return Object of type String of Label |
| */ |
| private Object deserializeStringToObject(String value) { |
| // A string represents a label if and only if it starts with "@@". |
| if (value.startsWith("@@")) { |
| return Label.parseCanonicalUnchecked(value); |
| } |
| // Strings that start and end with the escape sequence always require one layer to be stripped. |
| if (value.startsWith(STRING_ESCAPE_SEQUENCE) && value.endsWith(STRING_ESCAPE_SEQUENCE)) { |
| return value.substring( |
| STRING_ESCAPE_SEQUENCE.length(), value.length() - STRING_ESCAPE_SEQUENCE.length()); |
| } |
| return value; |
| } |
| } |