1package main
2
3import (
4	"flag"
5	"fmt"
6	"io"
7	"log"
8	"os"
9	"sort"
10	"strings"
11	"sync"
12
13	"android/soong/testing/code_metadata_internal_proto"
14	"android/soong/testing/code_metadata_proto"
15	"android/soong/testing/test_spec_proto"
16	"google.golang.org/protobuf/proto"
17)
18
19type keyToLocksMap struct {
20	locks sync.Map
21}
22
23func (kl *keyToLocksMap) GetLockForKey(key string) *sync.Mutex {
24	mutex, _ := kl.locks.LoadOrStore(key, &sync.Mutex{})
25	return mutex.(*sync.Mutex)
26}
27
28// Define a struct to hold the combination of team ID and multi-ownership flag for validation
29type sourceFileAttributes struct {
30	TeamID         string
31	MultiOwnership bool
32	Path           string
33}
34
35func getSortedKeys(syncMap *sync.Map) []string {
36	var allKeys []string
37	syncMap.Range(
38		func(key, _ interface{}) bool {
39			allKeys = append(allKeys, key.(string))
40			return true
41		},
42	)
43
44	sort.Strings(allKeys)
45	return allKeys
46}
47
48// writeProtoToFile marshals a protobuf message and writes it to a file
49func writeProtoToFile(outputFile string, message proto.Message) {
50	data, err := proto.Marshal(message)
51	if err != nil {
52		log.Fatal(err)
53	}
54	file, err := os.Create(outputFile)
55	if err != nil {
56		log.Fatal(err)
57	}
58	defer file.Close()
59
60	_, err = file.Write(data)
61	if err != nil {
62		log.Fatal(err)
63	}
64}
65
66func readFileToString(filePath string) string {
67	file, err := os.Open(filePath)
68	if err != nil {
69		log.Fatal(err)
70	}
71	defer file.Close()
72
73	data, err := io.ReadAll(file)
74	if err != nil {
75		log.Fatal(err)
76	}
77	return string(data)
78}
79
80func writeEmptyOutputProto(outputFile string, metadataRule string) {
81	file, err := os.Create(outputFile)
82	if err != nil {
83		log.Fatal(err)
84	}
85	var message proto.Message
86	if metadataRule == "test_spec" {
87		message = &test_spec_proto.TestSpec{}
88	} else if metadataRule == "code_metadata" {
89		message = &code_metadata_proto.CodeMetadata{}
90	}
91	data, err := proto.Marshal(message)
92	if err != nil {
93		log.Fatal(err)
94	}
95	defer file.Close()
96
97	_, err = file.Write([]byte(data))
98	if err != nil {
99		log.Fatal(err)
100	}
101}
102
103func processTestSpecProtobuf(
104	filePath string, ownershipMetadataMap *sync.Map, keyLocks *keyToLocksMap,
105	errCh chan error, wg *sync.WaitGroup,
106) {
107	defer wg.Done()
108
109	fileContent := strings.TrimRight(readFileToString(filePath), "\n")
110	testData := test_spec_proto.TestSpec{}
111	err := proto.Unmarshal([]byte(fileContent), &testData)
112	if err != nil {
113		errCh <- err
114		return
115	}
116
117	ownershipMetadata := testData.GetOwnershipMetadataList()
118	for _, metadata := range ownershipMetadata {
119		key := metadata.GetTargetName()
120		lock := keyLocks.GetLockForKey(key)
121		lock.Lock()
122
123		value, loaded := ownershipMetadataMap.LoadOrStore(
124			key, []*test_spec_proto.TestSpec_OwnershipMetadata{metadata},
125		)
126		if loaded {
127			existingMetadata := value.([]*test_spec_proto.TestSpec_OwnershipMetadata)
128			isDuplicate := false
129			for _, existing := range existingMetadata {
130				if metadata.GetTrendyTeamId() != existing.GetTrendyTeamId() {
131					errCh <- fmt.Errorf(
132						"Conflicting trendy team IDs found for %s at:\n%s with teamId"+
133							": %s,\n%s with teamId: %s",
134						key,
135						metadata.GetPath(), metadata.GetTrendyTeamId(), existing.GetPath(),
136						existing.GetTrendyTeamId(),
137					)
138
139					lock.Unlock()
140					return
141				}
142				if metadata.GetTrendyTeamId() == existing.GetTrendyTeamId() && metadata.GetPath() == existing.GetPath() {
143					isDuplicate = true
144					break
145				}
146			}
147			if !isDuplicate {
148				existingMetadata = append(existingMetadata, metadata)
149				ownershipMetadataMap.Store(key, existingMetadata)
150			}
151		}
152
153		lock.Unlock()
154	}
155}
156
157// processCodeMetadataProtobuf processes CodeMetadata protobuf files
158func processCodeMetadataProtobuf(
159	filePath string, ownershipMetadataMap *sync.Map, sourceFileMetadataMap *sync.Map, keyLocks *keyToLocksMap,
160	errCh chan error, wg *sync.WaitGroup,
161) {
162	defer wg.Done()
163
164	fileContent := strings.TrimRight(readFileToString(filePath), "\n")
165	internalCodeData := code_metadata_internal_proto.CodeMetadataInternal{}
166	err := proto.Unmarshal([]byte(fileContent), &internalCodeData)
167	if err != nil {
168		errCh <- err
169		return
170	}
171
172	// Process each TargetOwnership entry
173	for _, internalMetadata := range internalCodeData.GetTargetOwnershipList() {
174		key := internalMetadata.GetTargetName()
175		lock := keyLocks.GetLockForKey(key)
176		lock.Lock()
177
178		for _, srcFile := range internalMetadata.GetSourceFiles() {
179			srcFileKey := srcFile
180			srcFileLock := keyLocks.GetLockForKey(srcFileKey)
181			srcFileLock.Lock()
182			attributes := sourceFileAttributes{
183				TeamID:         internalMetadata.GetTrendyTeamId(),
184				MultiOwnership: internalMetadata.GetMultiOwnership(),
185				Path:           internalMetadata.GetPath(),
186			}
187
188			existingAttributes, exists := sourceFileMetadataMap.Load(srcFileKey)
189			if exists {
190				existing := existingAttributes.(sourceFileAttributes)
191				if attributes.TeamID != existing.TeamID && (!attributes.MultiOwnership || !existing.MultiOwnership) {
192					errCh <- fmt.Errorf(
193						"Conflict found for source file %s covered at %s with team ID: %s. Existing team ID: %s and path: %s."+
194							" If multi-ownership is required, multiOwnership should be set to true in all test_spec modules using this target. "+
195							"Multiple-ownership in general is discouraged though as it make infrastructure around android relying on this information pick up a random value when it needs only one.",
196						srcFile, internalMetadata.GetPath(), attributes.TeamID, existing.TeamID, existing.Path,
197					)
198					srcFileLock.Unlock()
199					lock.Unlock()
200					return
201				}
202			} else {
203				// Store the metadata if no conflict
204				sourceFileMetadataMap.Store(srcFileKey, attributes)
205			}
206			srcFileLock.Unlock()
207		}
208
209		value, loaded := ownershipMetadataMap.LoadOrStore(
210			key, []*code_metadata_internal_proto.CodeMetadataInternal_TargetOwnership{internalMetadata},
211		)
212		if loaded {
213			existingMetadata := value.([]*code_metadata_internal_proto.CodeMetadataInternal_TargetOwnership)
214			isDuplicate := false
215			for _, existing := range existingMetadata {
216				if internalMetadata.GetTrendyTeamId() == existing.GetTrendyTeamId() && internalMetadata.GetPath() == existing.GetPath() {
217					isDuplicate = true
218					break
219				}
220			}
221			if !isDuplicate {
222				existingMetadata = append(existingMetadata, internalMetadata)
223				ownershipMetadataMap.Store(key, existingMetadata)
224			}
225		}
226
227		lock.Unlock()
228	}
229}
230
231func main() {
232	inputFile := flag.String("inputFile", "", "Input file path")
233	outputFile := flag.String("outputFile", "", "Output file path")
234	rule := flag.String(
235		"rule", "", "Metadata rule (Hint: test_spec or code_metadata)",
236	)
237	flag.Parse()
238
239	if *inputFile == "" || *outputFile == "" || *rule == "" {
240		fmt.Println("Usage: metadata -rule <rule> -inputFile <input file path> -outputFile <output file path>")
241		os.Exit(1)
242	}
243
244	inputFileData := strings.TrimRight(readFileToString(*inputFile), "\n")
245	filePaths := strings.Split(inputFileData, " ")
246	if len(filePaths) == 1 && filePaths[0] == "" {
247		writeEmptyOutputProto(*outputFile, *rule)
248		return
249	}
250	ownershipMetadataMap := &sync.Map{}
251	keyLocks := &keyToLocksMap{}
252	errCh := make(chan error, len(filePaths))
253	var wg sync.WaitGroup
254
255	switch *rule {
256	case "test_spec":
257		for _, filePath := range filePaths {
258			wg.Add(1)
259			go processTestSpecProtobuf(
260				filePath, ownershipMetadataMap, keyLocks, errCh, &wg,
261			)
262		}
263
264		wg.Wait()
265		close(errCh)
266
267		for err := range errCh {
268			log.Fatal(err)
269		}
270
271		allKeys := getSortedKeys(ownershipMetadataMap)
272		var allMetadata []*test_spec_proto.TestSpec_OwnershipMetadata
273
274		for _, key := range allKeys {
275			value, _ := ownershipMetadataMap.Load(key)
276			metadataList := value.([]*test_spec_proto.TestSpec_OwnershipMetadata)
277			allMetadata = append(allMetadata, metadataList...)
278		}
279
280		testSpec := &test_spec_proto.TestSpec{
281			OwnershipMetadataList: allMetadata,
282		}
283		writeProtoToFile(*outputFile, testSpec)
284		break
285	case "code_metadata":
286		sourceFileMetadataMap := &sync.Map{}
287		for _, filePath := range filePaths {
288			wg.Add(1)
289			go processCodeMetadataProtobuf(
290				filePath, ownershipMetadataMap, sourceFileMetadataMap, keyLocks, errCh, &wg,
291			)
292		}
293
294		wg.Wait()
295		close(errCh)
296
297		for err := range errCh {
298			log.Fatal(err)
299		}
300
301		sortedKeys := getSortedKeys(ownershipMetadataMap)
302		allMetadata := make([]*code_metadata_proto.CodeMetadata_TargetOwnership, 0)
303		for _, key := range sortedKeys {
304			value, _ := ownershipMetadataMap.Load(key)
305			metadata := value.([]*code_metadata_internal_proto.CodeMetadataInternal_TargetOwnership)
306			for _, m := range metadata {
307				targetName := m.GetTargetName()
308				path := m.GetPath()
309				trendyTeamId := m.GetTrendyTeamId()
310
311				allMetadata = append(allMetadata, &code_metadata_proto.CodeMetadata_TargetOwnership{
312					TargetName:   &targetName,
313					Path:         &path,
314					TrendyTeamId: &trendyTeamId,
315					SourceFiles:  m.GetSourceFiles(),
316				})
317			}
318		}
319
320		finalMetadata := &code_metadata_proto.CodeMetadata{
321			TargetOwnershipList: allMetadata,
322		}
323		writeProtoToFile(*outputFile, finalMetadata)
324		break
325	default:
326		log.Fatalf("No specific processing implemented for rule '%s'.\n", *rule)
327	}
328}
329