blob: 433fc461e428fa295dcd45a3914f5cf3ef3cdeef [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package world
import (
"bytes"
"fmt"
"path/filepath"
"sort"
"strings"
"go.fuchsia.dev/fuchsia/tools/check-licenses/file"
"go.fuchsia.dev/fuchsia/tools/check-licenses/project"
)
// CSVData combines some header information along with the data for easily
// producing a CSV file.
//
// This just makes it easier for us to access the information from a template.
type CSVData struct {
Header string
Entries []*CSVEntry
}
type CSVEntry struct {
Project string
Path string
LicenseType string
Url string
Package string
Left string
Right string
// For compliance worksheet
BeingSurfaced string
SourceCodeIncluded string
}
func NewCSVEntry() {
}
func (csv *CSVEntry) Merge(other *CSVEntry) {
if !strings.Contains(csv.LicenseType, other.LicenseType+";") {
csv.LicenseType = fmt.Sprintf("%v; %v", csv.LicenseType, other.LicenseType)
}
if !strings.Contains(csv.Package, other.Package+";") {
csv.Package = fmt.Sprintf("%v; %v", csv.Package, other.Package)
}
csv.Left = fmt.Sprintf("%v; %v", csv.Left, other.Left)
}
func (w *World) GetMergedCSVEntries() *CSVData {
data := w.GetCSVEntries()
csvMap := make(map[string]*CSVEntry, 0)
for _, left := range data.Entries {
if right, ok := csvMap[left.Path]; ok {
right.Merge(left)
} else {
csvMap[left.Path] = left
}
}
newEntries := make([]*CSVEntry, 0)
for _, csv := range csvMap {
if csv.Package == csv.Project {
csv.Project = "Various"
}
newEntries = append(newEntries, csv)
}
sort.Slice(newEntries, func(i, j int) bool {
return newEntries[i].Path < newEntries[j].Path
})
data.Entries = newEntries
return data
}
func (w *World) GetCSVEntries() *CSVData {
csvData := &CSVData{}
entries := make([]*CSVEntry, 0)
// If world.Diff exists, it means we want to diff the NOTICE file from the current workspace
// against the diff file specified in Config.DiffTarget.
//
// In order to provide line numbers for each match in DiffTarget, find all of the line breaks
// in the target file by searching for newline characters.
newlines := make([]int, 0)
if w.Diff != nil {
for i := 0; i < len(w.Diff.Content); i++ {
index := bytes.Index(w.Diff.Content[i:], []byte("\n"))
if index < 0 {
break
}
i = i + index
newlines = append(newlines, i)
}
}
// If Config.DiffTarget is set, store information
// about the diff target in the return value.
sort.Sort(project.Order(w.FilteredProjects))
numLicenses := 0
numFound := 0
numPartiallyFound := 0
numMissing := 0
for _, p := range w.FilteredProjects {
sort.Sort(file.Order(p.LicenseFile))
for _, l := range p.LicenseFile {
sort.Sort(file.OrderFileData(l.Data))
for _, d := range l.Data {
numLicenses += 1
e := &CSVEntry{
Project: p.Name,
Path: l.Path,
Package: d.LibraryName,
LicenseType: d.LicenseType,
BeingSurfaced: "Yes",
SourceCodeIncluded: "No",
}
if e.Package == "" {
e.Package = e.Project
}
if strings.Contains(e.Path, Config.FuchsiaDir) {
e.Path, _ = filepath.Rel(Config.FuchsiaDir, e.Path)
}
if !p.ShouldBeDisplayed {
e.BeingSurfaced = "No"
}
if p.SourceCodeIncluded {
e.SourceCodeIncluded = "Yes"
}
if l.Url != "" {
e.Url = fmt.Sprintf(`=HYPERLINK("%v", "%v")`, l.Url, e.Path)
}
e.Left = fmt.Sprintf("line %v", d.LineNumber)
if w.Diff != nil {
status := "missing"
// Search for the given license text in the target notice file.
index := bytes.Index(w.Diff.Content, d.Data)
ln := getLineNumber(index, newlines)
if ln > -1 {
numFound += 1
status = fmt.Sprintf("line %v", ln)
} else {
// If it isn't found, perhaps the copyright header information
// was extracted & printed elsewhere. Try searching again,
// using only the license text (the bottom 80% of the filedata content).
size := len(d.Data)
start := int(0.2 * float64(size))
// It's possible the bottom 80% of the text matches in several places
// in the NOTICE file. Keep track of them all by using a "partials" slice.
partials := make([]int, 0)
index := bytes.Index(w.Diff.Content, d.Data[start:])
base := 0
for index > 0 {
ln := getLineNumber(index+base, newlines)
if ln > -1 {
partials = append(partials, ln)
}
base = base + index
index = bytes.Index(w.Diff.Content[base+1:], d.Data[start:])
}
if len(partials) > 0 {
numPartiallyFound += 1
status = fmt.Sprintf("partial line ")
for _, i := range partials {
status += fmt.Sprintf("%v | ", i)
}
} else {
numMissing += 1
}
}
e.Right = status
}
entries = append(entries, e)
}
}
}
if w.Diff != nil {
csvData.Header = fmt.Sprintf("Found %v licenses out of %v | partial found %v [missing %v]", numFound, numLicenses, numPartiallyFound, numMissing)
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Path < entries[j].Path
})
csvData.Entries = entries
return csvData
}
// index is the location in a byte slice where a certain text segment
// was found.
//
// newLines is the locations of all the newline characters in the entire
// file.
//
// This method simply returns the line number of the given piece of text,
// without us having to scan the entire file line-by-line.
func getLineNumber(index int, newlines []int) int {
if index < 0 {
return index
}
largest := 0
for i, v := range newlines {
if v == index {
return i
}
if v > index {
if i > 0 {
return i - 1
} else {
return 0
}
}
largest = i
}
return largest
}