blob: 50f808bb16ed23dda91b1d643e0afd73e2307865 [file] [log] [blame]
Bob Badoure6fdd142021-12-09 22:10:43 -08001// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package compliance
16
17import (
18 "bufio"
19 "crypto/md5"
20 "fmt"
21 "io"
22 "io/fs"
Bob Badour5028abc2022-02-09 11:56:58 -080023 "net/url"
Bob Badoure6fdd142021-12-09 22:10:43 -080024 "path/filepath"
25 "regexp"
26 "sort"
27 "strings"
28)
29
30const (
31 noProjectName = "\u2205"
32)
33
34var (
35 nameRegexp = regexp.MustCompile(`^\s*name\s*:\s*"(.*)"\s*$`)
36 descRegexp = regexp.MustCompile(`^\s*description\s*:\s*"(.*)"\s*$`)
37 versionRegexp = regexp.MustCompile(`^\s*version\s*:\s*"(.*)"\s*$`)
38 licensesPathRegexp = regexp.MustCompile(`licen[cs]es?/`)
39)
40
41// NoticeIndex transforms license metadata into license text hashes, library
42// names, and install paths indexing them for fast lookup/iteration.
43type NoticeIndex struct {
44 // lg identifies the license graph to which the index applies.
45 lg *LicenseGraph
46 // rs identifies the set of resolutions upon which the index is based.
47 rs ResolutionSet
48 // shipped identifies the set of target nodes shipped directly or as derivative works.
49 shipped *TargetNodeSet
50 // rootFS locates the root of the file system from which to read the files.
51 rootFS fs.FS
52 // hash maps license text filenames to content hashes
53 hash map[string]hash
54 // text maps content hashes to content
55 text map[hash][]byte
56 // hashLibInstall maps hashes to libraries to install paths.
57 hashLibInstall map[hash]map[string]map[string]struct{}
Bob Badour6ea14572022-01-23 17:15:46 -080058 // installHashLib maps install paths to libraries to hashes.
59 installHashLib map[string]map[hash]map[string]struct{}
Bob Badoure6fdd142021-12-09 22:10:43 -080060 // libHash maps libraries to hashes.
61 libHash map[string]map[hash]struct{}
62 // targetHash maps target nodes to hashes.
63 targetHashes map[*TargetNode]map[hash]struct{}
64 // projectName maps project directory names to project name text.
65 projectName map[string]string
Colin Crossbb45f8c2022-01-28 15:18:19 -080066 // files lists all the files accessed during indexing
67 files []string
Bob Badoure6fdd142021-12-09 22:10:43 -080068}
69
70// IndexLicenseTexts creates a hashed index of license texts for `lg` and `rs`
71// using the files rooted at `rootFS`.
72func IndexLicenseTexts(rootFS fs.FS, lg *LicenseGraph, rs ResolutionSet) (*NoticeIndex, error) {
73 if rs == nil {
74 rs = ResolveNotices(lg)
75 }
76 ni := &NoticeIndex{
Colin Crossbb45f8c2022-01-28 15:18:19 -080077 lg: lg,
78 rs: rs,
79 shipped: ShippedNodes(lg),
80 rootFS: rootFS,
81 hash: make(map[string]hash),
82 text: make(map[hash][]byte),
83 hashLibInstall: make(map[hash]map[string]map[string]struct{}),
84 installHashLib: make(map[string]map[hash]map[string]struct{}),
85 libHash: make(map[string]map[hash]struct{}),
86 targetHashes: make(map[*TargetNode]map[hash]struct{}),
87 projectName: make(map[string]string),
Bob Badoure6fdd142021-12-09 22:10:43 -080088 }
89
90 // index adds all license texts for `tn` to the index.
91 index := func(tn *TargetNode) (map[hash]struct{}, error) {
92 if hashes, ok := ni.targetHashes[tn]; ok {
93 return hashes, nil
94 }
95 hashes := make(map[hash]struct{})
96 for _, text := range tn.LicenseTexts() {
Bob Badour5028abc2022-02-09 11:56:58 -080097 fname := strings.SplitN(text, ":", 2)[0]
98 if _, ok := ni.hash[fname]; !ok {
99 err := ni.addText(fname)
Bob Badoure6fdd142021-12-09 22:10:43 -0800100 if err != nil {
101 return nil, err
102 }
103 }
Bob Badour5028abc2022-02-09 11:56:58 -0800104 hash := ni.hash[fname]
Bob Badoure6fdd142021-12-09 22:10:43 -0800105 if _, ok := hashes[hash]; !ok {
106 hashes[hash] = struct{}{}
107 }
108 }
109 ni.targetHashes[tn] = hashes
110 return hashes, nil
111 }
112
Bob Badour5028abc2022-02-09 11:56:58 -0800113 link := func(tn *TargetNode, hashes map[hash]struct{}, installPaths []string) {
Bob Badoure6fdd142021-12-09 22:10:43 -0800114 for h := range hashes {
Bob Badour5028abc2022-02-09 11:56:58 -0800115 libName := ni.getLibName(tn, h)
116 if _, ok := ni.libHash[libName]; !ok {
117 ni.libHash[libName] = make(map[hash]struct{})
118 }
Bob Badoure6fdd142021-12-09 22:10:43 -0800119 if _, ok := ni.hashLibInstall[h]; !ok {
120 ni.hashLibInstall[h] = make(map[string]map[string]struct{})
121 }
122 if _, ok := ni.libHash[libName][h]; !ok {
123 ni.libHash[libName][h] = struct{}{}
124 }
125 for _, installPath := range installPaths {
Bob Badour6ea14572022-01-23 17:15:46 -0800126 if _, ok := ni.installHashLib[installPath]; !ok {
127 ni.installHashLib[installPath] = make(map[hash]map[string]struct{})
128 ni.installHashLib[installPath][h] = make(map[string]struct{})
129 ni.installHashLib[installPath][h][libName] = struct{}{}
130 } else if _, ok = ni.installHashLib[installPath][h]; !ok {
131 ni.installHashLib[installPath][h] = make(map[string]struct{})
132 ni.installHashLib[installPath][h][libName] = struct{}{}
133 } else if _, ok = ni.installHashLib[installPath][h][libName]; !ok {
134 ni.installHashLib[installPath][h][libName] = struct{}{}
Bob Badoure6fdd142021-12-09 22:10:43 -0800135 }
136 if _, ok := ni.hashLibInstall[h]; !ok {
137 ni.hashLibInstall[h] = make(map[string]map[string]struct{})
138 ni.hashLibInstall[h][libName] = make(map[string]struct{})
139 ni.hashLibInstall[h][libName][installPath] = struct{}{}
140 } else if _, ok = ni.hashLibInstall[h][libName]; !ok {
141 ni.hashLibInstall[h][libName] = make(map[string]struct{})
142 ni.hashLibInstall[h][libName][installPath] = struct{}{}
143 } else if _, ok = ni.hashLibInstall[h][libName][installPath]; !ok {
144 ni.hashLibInstall[h][libName][installPath] = struct{}{}
145 }
146 }
147 }
148 }
149
150 // returns error from walk below.
151 var err error
152
153 WalkTopDown(NoEdgeContext{}, lg, func(lg *LicenseGraph, tn *TargetNode, path TargetEdgePath) bool {
154 if err != nil {
155 return false
156 }
157 if !ni.shipped.Contains(tn) {
158 return false
159 }
160 installPaths := getInstallPaths(tn, path)
161 var hashes map[hash]struct{}
162 hashes, err = index(tn)
163 if err != nil {
164 return false
165 }
Bob Badour5028abc2022-02-09 11:56:58 -0800166 link(tn, hashes, installPaths)
Bob Badoure6fdd142021-12-09 22:10:43 -0800167 if tn.IsContainer() {
168 return true
169 }
170
171 for _, r := range rs.Resolutions(tn) {
172 hashes, err = index(r.actsOn)
173 if err != nil {
174 return false
175 }
Bob Badour5028abc2022-02-09 11:56:58 -0800176 link(r.actsOn, hashes, installPaths)
Bob Badoure6fdd142021-12-09 22:10:43 -0800177 }
178 return false
179 })
180
181 if err != nil {
182 return nil, err
183 }
184
185 return ni, nil
186}
187
188// Hashes returns an ordered channel of the hashed license texts.
189func (ni *NoticeIndex) Hashes() chan hash {
190 c := make(chan hash)
191 go func() {
192 libs := make([]string, 0, len(ni.libHash))
193 for libName := range ni.libHash {
194 libs = append(libs, libName)
195 }
196 sort.Strings(libs)
197 hashes := make(map[hash]struct{})
198 for _, libName := range libs {
199 hl := make([]hash, 0, len(ni.libHash[libName]))
200 for h := range ni.libHash[libName] {
201 if _, ok := hashes[h]; ok {
202 continue
203 }
204 hashes[h] = struct{}{}
205 hl = append(hl, h)
206 }
207 if len(hl) > 0 {
Bob Badour6ea14572022-01-23 17:15:46 -0800208 sort.Sort(hashList{ni, libName, "", &hl})
Bob Badoure6fdd142021-12-09 22:10:43 -0800209 for _, h := range hl {
210 c <- h
211 }
212 }
213 }
214 close(c)
215 }()
216 return c
217}
218
Colin Crossbb45f8c2022-01-28 15:18:19 -0800219// InputNoticeFiles returns the list of files that were hashed during IndexLicenseTexts.
220func (ni *NoticeIndex) InputNoticeFiles() []string {
221 files := append([]string(nil), ni.files...)
222 sort.Strings(files)
223 return files
224}
225
Bob Badoure6fdd142021-12-09 22:10:43 -0800226// HashLibs returns the ordered array of library names using the license text
227// hashed as `h`.
228func (ni *NoticeIndex) HashLibs(h hash) []string {
229 libs := make([]string, 0, len(ni.hashLibInstall[h]))
230 for libName := range ni.hashLibInstall[h] {
231 libs = append(libs, libName)
232 }
233 sort.Strings(libs)
234 return libs
235}
236
237// HashLibInstalls returns the ordered array of install paths referencing
238// library `libName` using the license text hashed as `h`.
239func (ni *NoticeIndex) HashLibInstalls(h hash, libName string) []string {
240 installs := make([]string, 0, len(ni.hashLibInstall[h][libName]))
241 for installPath := range ni.hashLibInstall[h][libName] {
242 installs = append(installs, installPath)
243 }
244 sort.Strings(installs)
245 return installs
246}
247
Bob Badour6ea14572022-01-23 17:15:46 -0800248// InstallPaths returns the ordered channel of indexed install paths.
249func (ni *NoticeIndex) InstallPaths() chan string {
250 c := make(chan string)
251 go func() {
252 paths := make([]string, 0, len(ni.installHashLib))
253 for path := range ni.installHashLib {
254 paths = append(paths, path)
255 }
256 sort.Strings(paths)
257 for _, installPath := range paths {
258 c <- installPath
259 }
260 close(c)
261 }()
262 return c
263}
264
265// InstallHashes returns the ordered array of hashes attached to `installPath`.
266func (ni *NoticeIndex) InstallHashes(installPath string) []hash {
267 result := make([]hash, 0, len(ni.installHashLib[installPath]))
268 for h := range ni.installHashLib[installPath] {
269 result = append(result, h)
270 }
271 if len(result) > 0 {
272 sort.Sort(hashList{ni, "", installPath, &result})
273 }
274 return result
275}
276
277// InstallHashLibs returns the ordered array of library names attached to
278// `installPath` as hash `h`.
279func (ni *NoticeIndex) InstallHashLibs(installPath string, h hash) []string {
280 result := make([]string, 0, len(ni.installHashLib[installPath][h]))
281 for libName := range ni.installHashLib[installPath][h] {
282 result = append(result, libName)
283 }
284 sort.Strings(result)
285 return result
286}
287
Bob Badour00c8a382022-01-26 17:21:39 -0800288// Libraries returns the ordered channel of indexed library names.
289func (ni *NoticeIndex) Libraries() chan string {
290 c := make(chan string)
291 go func() {
292 libs := make([]string, 0, len(ni.libHash))
293 for lib := range ni.libHash {
294 libs = append(libs, lib)
295 }
296 sort.Strings(libs)
297 for _, lib := range libs {
298 c <- lib
299 }
300 close(c)
301 }()
302 return c
303}
304
Bob Badoure6fdd142021-12-09 22:10:43 -0800305// HashText returns the file content of the license text hashed as `h`.
306func (ni *NoticeIndex) HashText(h hash) []byte {
307 return ni.text[h]
308}
309
310// getLibName returns the name of the library associated with `noticeFor`.
Bob Badour5028abc2022-02-09 11:56:58 -0800311func (ni *NoticeIndex) getLibName(noticeFor *TargetNode, h hash) string {
312 for _, text := range noticeFor.LicenseTexts() {
313 if !strings.Contains(text, ":") {
Bob Badour77570052022-02-28 20:05:44 -0800314 if ni.hash[text].key != h.key {
315 continue
316 }
317 ln := ni.checkMetadataForLicenseText(noticeFor, text)
318 if len(ln) > 0 {
319 return ln
320 }
Bob Badour5028abc2022-02-09 11:56:58 -0800321 continue
322 }
323
324 fields := strings.SplitN(text, ":", 2)
325 fname, pname := fields[0], fields[1]
326 if ni.hash[fname].key != h.key {
327 continue
328 }
329
330 ln, err := url.QueryUnescape(pname)
331 if err != nil {
332 continue
333 }
334 return ln
335 }
Bob Badoure6fdd142021-12-09 22:10:43 -0800336 // use name from METADATA if available
337 ln := ni.checkMetadata(noticeFor)
338 if len(ln) > 0 {
339 return ln
340 }
341 // use package_name: from license{} module if available
342 pn := noticeFor.PackageName()
343 if len(pn) > 0 {
344 return pn
345 }
346 for _, p := range noticeFor.Projects() {
347 if strings.HasPrefix(p, "prebuilts/") {
348 for _, licenseText := range noticeFor.LicenseTexts() {
349 if !strings.HasPrefix(licenseText, "prebuilts/") {
350 continue
351 }
Bob Badour77570052022-02-28 20:05:44 -0800352 if !strings.Contains(licenseText, ":") {
353 if ni.hash[licenseText].key != h.key {
354 continue
355 }
356 } else {
357 fields := strings.SplitN(licenseText, ":", 2)
358 fname := fields[0]
359 if ni.hash[fname].key != h.key {
360 continue
361 }
362 }
Bob Badour113c92b2022-09-23 11:27:24 -0700363 for _, r := range OrderedSafePrebuiltPrefixes {
364 prefix := SafePrebuiltPrefixes[r]
Bob Badoure6fdd142021-12-09 22:10:43 -0800365 match := r.FindString(licenseText)
366 if len(match) == 0 {
367 continue
368 }
369 strip := SafePathPrefixes[prefix]
370 if strip {
371 // strip entire prefix
372 match = licenseText[len(match):]
373 } else {
374 // strip from prebuilts/ until safe prefix
375 match = licenseText[len(match)-len(prefix):]
376 }
377 // remove LICENSE or NOTICE or other filename
378 li := strings.LastIndex(match, "/")
Bob Badoure9b38c12022-02-09 15:56:59 -0800379 if li > 0 {
Bob Badoure6fdd142021-12-09 22:10:43 -0800380 match = match[:li]
381 }
382 // remove *licenses/ path segment and subdirectory if in path
Bob Badoure9b38c12022-02-09 15:56:59 -0800383 if offsets := licensesPathRegexp.FindAllStringIndex(match, -1); offsets != nil && offsets[len(offsets)-1][0] > 0 {
Bob Badoure6fdd142021-12-09 22:10:43 -0800384 match = match[:offsets[len(offsets)-1][0]]
385 li = strings.LastIndex(match, "/")
Bob Badoure9b38c12022-02-09 15:56:59 -0800386 if li > 0 {
Bob Badoure6fdd142021-12-09 22:10:43 -0800387 match = match[:li]
388 }
389 }
390 return match
391 }
392 break
393 }
394 }
395 for prefix, strip := range SafePathPrefixes {
396 if strings.HasPrefix(p, prefix) {
397 if strip {
398 return p[len(prefix):]
399 } else {
400 return p
401 }
402 }
403 }
404 }
405 // strip off [./]meta_lic from license metadata path and extract base name
406 n := noticeFor.name[:len(noticeFor.name)-9]
407 li := strings.LastIndex(n, "/")
Bob Badoure9b38c12022-02-09 15:56:59 -0800408 if li > 0 {
Bob Badoure6fdd142021-12-09 22:10:43 -0800409 n = n[li+1:]
410 }
Bob Badour77570052022-02-28 20:05:44 -0800411 fi := strings.Index(n, "@")
412 if fi > 0 {
413 n = n[:fi]
414 }
Bob Badoure6fdd142021-12-09 22:10:43 -0800415 return n
416}
417
418// checkMetadata tries to look up a library name from a METADATA file associated with `noticeFor`.
419func (ni *NoticeIndex) checkMetadata(noticeFor *TargetNode) string {
420 for _, p := range noticeFor.Projects() {
421 if name, ok := ni.projectName[p]; ok {
422 if name == noProjectName {
423 continue
424 }
425 return name
426 }
Bob Badour77570052022-02-28 20:05:44 -0800427 name, err := ni.checkMetadataFile(filepath.Join(p, "METADATA"))
Bob Badoure6fdd142021-12-09 22:10:43 -0800428 if err != nil {
429 ni.projectName[p] = noProjectName
430 continue
431 }
Bob Badour77570052022-02-28 20:05:44 -0800432 if len(name) == 0 {
433 ni.projectName[p] = noProjectName
434 continue
Bob Badoure6fdd142021-12-09 22:10:43 -0800435 }
Bob Badour77570052022-02-28 20:05:44 -0800436 ni.projectName[p] = name
437 return name
Bob Badoure6fdd142021-12-09 22:10:43 -0800438 }
439 return ""
440}
441
Bob Badour77570052022-02-28 20:05:44 -0800442// checkMetadataForLicenseText
443func (ni *NoticeIndex) checkMetadataForLicenseText(noticeFor *TargetNode, licenseText string) string {
444 p := ""
445 for _, proj := range noticeFor.Projects() {
446 if strings.HasPrefix(licenseText, proj) {
447 p = proj
448 }
449 }
450 if len(p) == 0 {
451 p = filepath.Dir(licenseText)
452 for {
453 fi, err := fs.Stat(ni.rootFS, filepath.Join(p, ".git"))
454 if err == nil && fi.IsDir() {
455 break
456 }
457 if strings.Contains(p, "/") && p != "/" {
458 p = filepath.Dir(p)
459 continue
460 }
461 return ""
462 }
463 }
464 if name, ok := ni.projectName[p]; ok {
465 if name == noProjectName {
466 return ""
467 }
468 return name
469 }
470 name, err := ni.checkMetadataFile(filepath.Join(p, "METADATA"))
471 if err == nil && len(name) > 0 {
472 ni.projectName[p] = name
473 return name
474 }
475 ni.projectName[p] = noProjectName
476 return ""
477}
478
479// checkMetadataFile tries to look up a library name from a METADATA file at `path`.
480func (ni *NoticeIndex) checkMetadataFile(path string) (string, error) {
481 f, err := ni.rootFS.Open(path)
482 if err != nil {
483 return "", err
484 }
485 name := ""
486 description := ""
487 version := ""
488 s := bufio.NewScanner(f)
489 for s.Scan() {
490 line := s.Text()
491 m := nameRegexp.FindStringSubmatch(line)
492 if m != nil {
493 if 1 < len(m) && m[1] != "" {
494 name = m[1]
495 }
496 if version != "" {
497 break
498 }
499 continue
500 }
501 m = versionRegexp.FindStringSubmatch(line)
502 if m != nil {
503 if 1 < len(m) && m[1] != "" {
504 version = m[1]
505 }
506 if name != "" {
507 break
508 }
509 continue
510 }
511 m = descRegexp.FindStringSubmatch(line)
512 if m != nil {
513 if 1 < len(m) && m[1] != "" {
514 description = m[1]
515 }
516 }
517 }
518 _ = s.Err()
519 _ = f.Close()
520 if name != "" {
521 if version != "" {
522 if version[0] == 'v' || version[0] == 'V' {
523 return name + "_" + version, nil
524 } else {
525 return name + "_v_" + version, nil
526 }
527 }
528 return name, nil
529 }
530 if description != "" {
531 return description, nil
532 }
533 return "", nil
534}
535
Bob Badoure6fdd142021-12-09 22:10:43 -0800536// addText reads and indexes the content of a license text file.
537func (ni *NoticeIndex) addText(file string) error {
538 f, err := ni.rootFS.Open(filepath.Clean(file))
539 if err != nil {
540 return fmt.Errorf("error opening license text file %q: %w", file, err)
541 }
542
543 // read the file
544 text, err := io.ReadAll(f)
545 if err != nil {
546 return fmt.Errorf("error reading license text file %q: %w", file, err)
547 }
548
549 hash := hash{fmt.Sprintf("%x", md5.Sum(text))}
550 ni.hash[file] = hash
551 if _, alreadyPresent := ni.text[hash]; !alreadyPresent {
552 ni.text[hash] = text
553 }
554
Colin Crossbb45f8c2022-01-28 15:18:19 -0800555 ni.files = append(ni.files, file)
556
Bob Badoure6fdd142021-12-09 22:10:43 -0800557 return nil
558}
559
560// getInstallPaths returns the names of the used dependencies mapped to their
561// installed locations.
562func getInstallPaths(attachesTo *TargetNode, path TargetEdgePath) []string {
563 if len(path) == 0 {
564 installs := attachesTo.Installed()
565 if 0 == len(installs) {
566 installs = attachesTo.Built()
567 }
568 return installs
569 }
570
571 var getInstalls func(path TargetEdgePath) []string
572
573 getInstalls = func(path TargetEdgePath) []string {
574 // deps contains the output targets from the dependencies in the path
575 var deps []string
576 if len(path) > 1 {
577 // recursively get the targets from the sub-path skipping 1 path segment
578 deps = getInstalls(path[1:])
579 } else {
580 // stop recursion at 1 path segment
581 deps = path[0].Dependency().TargetFiles()
582 }
583 size := 0
584 prefixes := path[0].Target().TargetFiles()
585 installMap := path[0].Target().InstallMap()
586 sources := path[0].Target().Sources()
587 for _, dep := range deps {
588 found := false
589 for _, source := range sources {
590 if strings.HasPrefix(dep, source) {
591 found = true
592 break
593 }
594 }
595 if !found {
596 continue
597 }
598 for _, im := range installMap {
599 if strings.HasPrefix(dep, im.FromPath) {
600 size += len(prefixes)
601 break
602 }
603 }
604 }
605
606 installs := make([]string, 0, size)
607 for _, dep := range deps {
608 found := false
609 for _, source := range sources {
610 if strings.HasPrefix(dep, source) {
611 found = true
612 break
613 }
614 }
615 if !found {
616 continue
617 }
618 for _, im := range installMap {
619 if strings.HasPrefix(dep, im.FromPath) {
620 for _, prefix := range prefixes {
621 installs = append(installs, prefix+im.ContainerPath+dep[len(im.FromPath):])
622 }
623 break
624 }
625 }
626 }
627 return installs
628 }
629 allInstalls := getInstalls(path)
630 installs := path[0].Target().Installed()
631 if len(installs) == 0 {
632 return allInstalls
633 }
634 result := make([]string, 0, len(allInstalls))
635 for _, install := range allInstalls {
636 for _, prefix := range installs {
637 if strings.HasPrefix(install, prefix) {
638 result = append(result, install)
639 }
640 }
641 }
642 return result
643}
644
645// hash is an opaque string derived from md5sum.
646type hash struct {
647 key string
648}
649
650// String returns the hexadecimal representation of the hash.
651func (h hash) String() string {
652 return h.key
653}
654
655// hashList orders an array of hashes
656type hashList struct {
Bob Badour6ea14572022-01-23 17:15:46 -0800657 ni *NoticeIndex
658 libName string
659 installPath string
660 hashes *[]hash
Bob Badoure6fdd142021-12-09 22:10:43 -0800661}
662
663// Len returns the count of elements in the slice.
664func (l hashList) Len() int { return len(*l.hashes) }
665
666// Swap rearranges 2 elements of the slice so that each occupies the other's
667// former position.
668func (l hashList) Swap(i, j int) { (*l.hashes)[i], (*l.hashes)[j] = (*l.hashes)[j], (*l.hashes)[i] }
669
670// Less returns true when the `i`th element is lexicographically less than
671// the `j`th element.
672func (l hashList) Less(i, j int) bool {
673 var insti, instj int
Bob Badoure9b38c12022-02-09 15:56:59 -0800674 if len(l.libName) > 0 {
Bob Badoure6fdd142021-12-09 22:10:43 -0800675 insti = len(l.ni.hashLibInstall[(*l.hashes)[i]][l.libName])
676 instj = len(l.ni.hashLibInstall[(*l.hashes)[j]][l.libName])
Bob Badour6ea14572022-01-23 17:15:46 -0800677 } else {
678 libsi := l.ni.InstallHashLibs(l.installPath, (*l.hashes)[i])
679 libsj := l.ni.InstallHashLibs(l.installPath, (*l.hashes)[j])
680 libsis := strings.Join(libsi, " ")
681 libsjs := strings.Join(libsj, " ")
682 if libsis != libsjs {
683 return libsis < libsjs
684 }
Bob Badoure6fdd142021-12-09 22:10:43 -0800685 }
686 if insti == instj {
687 leni := len(l.ni.text[(*l.hashes)[i]])
688 lenj := len(l.ni.text[(*l.hashes)[j]])
689 if leni == lenj {
690 // all else equal, just order by hash value
691 return (*l.hashes)[i].key < (*l.hashes)[j].key
692 }
693 // put shortest texts first within same # of installs
694 return leni < lenj
695 }
696 // reverse order of # installs so that most popular appears first
697 return instj < insti
698}