1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.build.content;
17 
18 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
19 import com.android.tradefed.log.LogUtil.CLog;
20 
21 import com.google.gson.Gson;
22 import com.google.gson.JsonArray;
23 import com.google.gson.JsonElement;
24 import com.google.gson.JsonObject;
25 
26 import java.io.BufferedReader;
27 import java.io.File;
28 import java.io.FileReader;
29 import java.io.IOException;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.stream.Collectors;
34 
35 /** This describes the structure of the content and its descriptor as generated by the CAS tool */
36 public class ArtifactDetails {
37     public String artifact; // Name of field has to match the cas tool output
38     public List<ArtifactFileDescriptor> details; // Name of field has to match the cas tool output
39 
40     public static class ArtifactFileDescriptor {
41         public String digest;
42         public String path;
43         public long size;
44     }
45 
parseFile(File input, String targetArtifact)46     public static ArtifactDetails parseFile(File input, String targetArtifact) throws IOException {
47         return parseFile(input, targetArtifact, null, null);
48     }
49 
50     /** Parses cas_content_details.json and extract information for the entry considered. */
parseFile( File input, String targetArtifact, String baseBuildId, String currentBuildId)51     public static ArtifactDetails parseFile(
52             File input, String targetArtifact, String baseBuildId, String currentBuildId)
53             throws IOException {
54         try (CloseableTraceScope ignored = new CloseableTraceScope("parse_artifacts_details")) {
55             Gson gson = new Gson();
56             JsonArray mainArray = null;
57             try (BufferedReader reader = new BufferedReader(new FileReader(input))) {
58                 mainArray = gson.fromJson(reader, JsonArray.class);
59             }
60             String namedArtifact = targetArtifact;
61             if (currentBuildId != null && targetArtifact.contains(currentBuildId)) {
62                 namedArtifact = namedArtifact.replace(currentBuildId, baseBuildId);
63             }
64             for (JsonElement e : mainArray) {
65                 JsonObject o = e.getAsJsonObject();
66                 JsonElement name = o.asMap().get("artifact");
67                 if (name.getAsString().equals(namedArtifact)) {
68                     ArtifactDetails d = gson.fromJson(e, ArtifactDetails.class);
69                     if (d == null) {
70                         throw new RuntimeException("Failed to parse for content.");
71                     }
72                     return d;
73                 }
74             }
75             throw new RuntimeException(namedArtifact + " entry was not found.");
76         }
77     }
78 
79     /** Obtain the list of modification between a base and the current build contents. */
diffContents( ArtifactDetails base, ArtifactDetails current)80     public static List<ArtifactFileDescriptor> diffContents(
81             ArtifactDetails base, ArtifactDetails current) {
82         try (CloseableTraceScope ignored = new CloseableTraceScope("diff_contents")) {
83             if (!base.artifact.equals(current.artifact)) {
84                 CLog.w(
85                         "Not comparing the same artifact entries ! %s != %s",
86                         base.artifact, current.artifact);
87             }
88             Map<String, ArtifactFileDescriptor> mappingBase =
89                     base.details.stream()
90                             .map(e -> e)
91                             .collect(Collectors.toMap(e -> e.path, e -> e));
92             Map<String, ArtifactFileDescriptor> mappingCurrent =
93                     current.details.stream()
94                             .map(e -> e)
95                             .collect(Collectors.toMap(e -> e.path, e -> e));
96             List<ArtifactFileDescriptor> affected = new ArrayList<>();
97             int modified = 0;
98             int deleted = 0;
99             int added = 0;
100             int unchanged = 0;
101             for (ArtifactFileDescriptor be : base.details) {
102                 if (mappingCurrent.containsKey(be.path)) {
103                     ArtifactFileDescriptor pe = mappingCurrent.get(be.path);
104                     if (!be.digest.equals(pe.digest)) {
105                         CLog.d("diff: %s", be.path);
106                         modified++;
107                         affected.add(pe);
108                     } else {
109                         unchanged++;
110                     }
111                 } else {
112                     affected.add(be);
113                     CLog.d("deleted: %s", be.path);
114                     deleted++;
115                 }
116             }
117             for (ArtifactFileDescriptor pe : current.details) {
118                 if (!mappingBase.containsKey(pe.path)) {
119                     CLog.d("added: %s", pe.path);
120                     added++;
121                     affected.add(pe);
122                 }
123             }
124             CLog.d(
125                     "Summary: unchanged:%s, modified:%s, deleted:%s, added:%s",
126                     unchanged, modified, deleted, added);
127             return affected;
128         }
129     }
130 }
131