Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b816e586 authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "license metadata html notice files"

parents 29429f92 6ea1457c
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -45,6 +45,13 @@ blueprint_go_binary {
    testSrcs: ["cmd/dumpresolutions_test.go"],
}

blueprint_go_binary {
    name: "htmlnotice",
    srcs: ["cmd/htmlnotice.go"],
    deps: ["compliance-module"],
    testSrcs: ["cmd/htmlnotice_test.go"],
}

blueprint_go_binary {
    name: "textnotice",
    srcs: ["cmd/textnotice.go"],
+216 −0
Original line number Diff line number Diff line
// Copyright 2021 Google LLC
//
// 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 main

import (
	"bytes"
	"compliance"
	"flag"
	"fmt"
	"html"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

var (
	outputFile  = flag.String("o", "-", "Where to write the NOTICE text file. (default stdout)")
	includeTOC  = flag.Bool("toc", true, "Whether to include a table of contents.")
	stripPrefix = flag.String("strip_prefix", "", "Prefix to remove from paths. i.e. path to root")
	title       = flag.String("title", "", "The title of the notice file.")

	failNoneRequested = fmt.Errorf("\nNo license metadata files requested")
	failNoLicenses    = fmt.Errorf("No licenses found")
)

type context struct {
	stdout      io.Writer
	stderr      io.Writer
	rootFS      fs.FS
	includeTOC  bool
	stripPrefix string
	title       string
}

func init() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, `Usage: %s {options} file.meta_lic {file.meta_lic...}

Outputs an html NOTICE.html file.

Options:
`, filepath.Base(os.Args[0]))
		flag.PrintDefaults()
	}
}

func main() {
	flag.Parse()

	// Must specify at least one root target.
	if flag.NArg() == 0 {
		flag.Usage()
		os.Exit(2)
	}

	if len(*outputFile) == 0 {
		flag.Usage()
		fmt.Fprintf(os.Stderr, "must specify file for -o; use - for stdout\n")
		os.Exit(2)
	} else {
		dir, err := filepath.Abs(filepath.Dir(*outputFile))
		if err != nil {
			fmt.Fprintf(os.Stderr, "cannot determine path to %q: %w\n", *outputFile, err)
			os.Exit(1)
		}
		fi, err := os.Stat(dir)
		if err != nil {
			fmt.Fprintf(os.Stderr, "cannot read directory %q of %q: %w\n", dir, *outputFile, err)
			os.Exit(1)
		}
		if !fi.IsDir() {
			fmt.Fprintf(os.Stderr, "parent %q of %q is not a directory\n", dir, *outputFile)
			os.Exit(1)
		}
	}

	var ofile io.Writer
	ofile = os.Stdout
	if *outputFile != "-" {
		ofile = &bytes.Buffer{}
	}

	ctx := &context{ofile, os.Stderr, os.DirFS("."), *includeTOC, *stripPrefix, *title}

	err := htmlNotice(ctx, flag.Args()...)
	if err != nil {
		if err == failNoneRequested {
			flag.Usage()
		}
		fmt.Fprintf(os.Stderr, "%s\n", err.Error())
		os.Exit(1)
	}
	if *outputFile != "-" {
		err := os.WriteFile(*outputFile, ofile.(*bytes.Buffer).Bytes(), 0666)
		if err != nil {
			fmt.Fprintf(os.Stderr, "could not write output to %q: %w\n", *outputFile, err)
			os.Exit(1)
		}
	}
	os.Exit(0)
}

// htmlNotice implements the htmlnotice utility.
func htmlNotice(ctx *context, files ...string) error {
	// Must be at least one root file.
	if len(files) < 1 {
		return failNoneRequested
	}

	// Read the license graph from the license metadata files (*.meta_lic).
	licenseGraph, err := compliance.ReadLicenseGraph(ctx.rootFS, ctx.stderr, files)
	if err != nil {
		return fmt.Errorf("Unable to read license metadata file(s) %q: %v\n", files, err)
	}
	if licenseGraph == nil {
		return failNoLicenses
	}

	// rs contains all notice resolutions.
	rs := compliance.ResolveNotices(licenseGraph)

	ni, err := compliance.IndexLicenseTexts(ctx.rootFS, licenseGraph, rs)
	if err != nil {
		return fmt.Errorf("Unable to read license text file(s) for %q: %v\n", files, err)
	}

	fmt.Fprintln(ctx.stdout, "<!DOCTYPE html>")
	fmt.Fprintln(ctx.stdout, "<html><head>\n")
	fmt.Fprintln(ctx.stdout, "<style type=\"text/css\">")
	fmt.Fprintln(ctx.stdout, "body { padding: 2px; margin: 0; }")
	fmt.Fprintln(ctx.stdout, "ul { list-style-type: none; margin: 0; padding: 0; }")
	fmt.Fprintln(ctx.stdout, "li { padding-left: 1em; }")
	fmt.Fprintln(ctx.stdout, ".file-list { margin-left: 1em; }")
	fmt.Fprintln(ctx.stdout, "</style>\n")
	if 0 < len(ctx.title) {
		fmt.Fprintf(ctx.stdout, "<title>%s</title>\n", html.EscapeString(ctx.title))
	}
	fmt.Fprintln(ctx.stdout, "</head>")
	fmt.Fprintln(ctx.stdout, "<body>")

	if 0 < len(ctx.title) {
		fmt.Fprintf(ctx.stdout, "  <h1>%s</h1>\n", html.EscapeString(ctx.title))
	}
	ids := make(map[string]string)
	if ctx.includeTOC {
		fmt.Fprintln(ctx.stdout, "  <ul class=\"toc\">")
		i := 0
		for installPath := range ni.InstallPaths() {
			id := fmt.Sprintf("id%d", i)
			i++
			ids[installPath] = id
			var p string
			if 0 < len(ctx.stripPrefix) && strings.HasPrefix(installPath, ctx.stripPrefix) {
				p = installPath[len(ctx.stripPrefix):]
				if 0 == len(p) {
					if 0 < len(ctx.title) {
						p = ctx.title
					} else {
						p = "root"
					}
				}
			} else {
				p = installPath
			}
			fmt.Fprintf(ctx.stdout, "    <li id=\"%s\"><strong>%s</strong>\n      <ul>\n", id, html.EscapeString(p))
			for _, h := range ni.InstallHashes(installPath) {
				libs := ni.InstallHashLibs(installPath, h)
				fmt.Fprintf(ctx.stdout, "        <li><a href=\"#%s\">%s</a>\n", h.String(), html.EscapeString(strings.Join(libs, ", ")))
			}
			fmt.Fprintln(ctx.stdout, "      </ul>")
		}
		fmt.Fprintln(ctx.stdout, "  </ul><!-- toc -->")
	}
	for h := range ni.Hashes() {
		fmt.Fprintln(ctx.stdout, "  <hr>")
		for _, libName := range ni.HashLibs(h) {
			fmt.Fprintf(ctx.stdout, "  <strong>%s</strong> used by:\n    <ul class=\"file-list\">\n", html.EscapeString(libName))
			for _, installPath := range ni.HashLibInstalls(h, libName) {
				if id, ok := ids[installPath]; ok {
					if 0 < len(ctx.stripPrefix) && strings.HasPrefix(installPath, ctx.stripPrefix) {
						fmt.Fprintf(ctx.stdout, "      <li><a href=\"#%s\">%s</a>\n", id, html.EscapeString(installPath[len(ctx.stripPrefix):]))
					} else {
						fmt.Fprintf(ctx.stdout, "      <li><a href=\"#%s\">%s</a>\n", id, html.EscapeString(installPath))
					}
				} else {
					if 0 < len(ctx.stripPrefix) && strings.HasPrefix(installPath, ctx.stripPrefix) {
						fmt.Fprintf(ctx.stdout, "      <li>%s\n", html.EscapeString(installPath[len(ctx.stripPrefix):]))
					} else {
						fmt.Fprintf(ctx.stdout, "      <li>%s\n", html.EscapeString(installPath))
					}
				}
			}
			fmt.Fprintf(ctx.stdout, "    </ul>\n")
		}
		fmt.Fprintf(ctx.stdout, "  </ul>\n  <a id=\"%s\"/><pre class=\"license-text\">", h.String())
		fmt.Fprintln(ctx.stdout, html.EscapeString(string(ni.HashText(h))))
		fmt.Fprintln(ctx.stdout, "  </pre><!-- license-text -->")
	}
	fmt.Fprintln(ctx.stdout, "</body></html>")

	return nil
}
+812 −0

File added.

Preview size limit exceeded, changes collapsed.

+65 −16
Original line number Diff line number Diff line
@@ -54,8 +54,8 @@ type NoticeIndex struct {
	text map[hash][]byte
	// hashLibInstall maps hashes to libraries to install paths.
	hashLibInstall map[hash]map[string]map[string]struct{}
	// installLibHash maps install paths to libraries to hashes.
	installLibHash map[string]map[string]map[hash]struct{}
	// installHashLib maps install paths to libraries to hashes.
	installHashLib map[string]map[hash]map[string]struct{}
	// libHash maps libraries to hashes.
	libHash map[string]map[hash]struct{}
	// targetHash maps target nodes to hashes.
@@ -75,7 +75,7 @@ func IndexLicenseTexts(rootFS fs.FS, lg *LicenseGraph, rs ResolutionSet) (*Notic
		make(map[string]hash),
		make(map[hash][]byte),
		make(map[hash]map[string]map[string]struct{}),
		make(map[string]map[string]map[hash]struct{}),
		make(map[string]map[hash]map[string]struct{}),
		make(map[string]map[hash]struct{}),
		make(map[*TargetNode]map[hash]struct{}),
		make(map[string]string),
@@ -115,15 +115,15 @@ func IndexLicenseTexts(rootFS fs.FS, lg *LicenseGraph, rs ResolutionSet) (*Notic
				ni.libHash[libName][h] = struct{}{}
			}
			for _, installPath := range installPaths {
				if _, ok := ni.installLibHash[installPath]; !ok {
					ni.installLibHash[installPath] = make(map[string]map[hash]struct{})
					ni.installLibHash[installPath][libName] = make(map[hash]struct{})
					ni.installLibHash[installPath][libName][h] = struct{}{}
				} else if _, ok = ni.installLibHash[installPath][libName]; !ok {
					ni.installLibHash[installPath][libName] = make(map[hash]struct{})
					ni.installLibHash[installPath][libName][h] = struct{}{}
				} else if _, ok = ni.installLibHash[installPath][libName][h]; !ok {
					ni.installLibHash[installPath][libName][h] = struct{}{}
				if _, ok := ni.installHashLib[installPath]; !ok {
					ni.installHashLib[installPath] = make(map[hash]map[string]struct{})
					ni.installHashLib[installPath][h] = make(map[string]struct{})
					ni.installHashLib[installPath][h][libName] = struct{}{}
				} else if _, ok = ni.installHashLib[installPath][h]; !ok {
					ni.installHashLib[installPath][h] = make(map[string]struct{})
					ni.installHashLib[installPath][h][libName] = struct{}{}
				} else if _, ok = ni.installHashLib[installPath][h][libName]; !ok {
					ni.installHashLib[installPath][h][libName] = struct{}{}
				}
				if _, ok := ni.hashLibInstall[h]; !ok {
					ni.hashLibInstall[h] = make(map[string]map[string]struct{})
@@ -197,7 +197,7 @@ func (ni *NoticeIndex) Hashes() chan hash {
				hl = append(hl, h)
			}
			if len(hl) > 0 {
				sort.Sort(hashList{ni, libName, &hl})
				sort.Sort(hashList{ni, libName, "", &hl})
				for _, h := range hl {
					c <- h
				}
@@ -230,6 +230,46 @@ func (ni *NoticeIndex) HashLibInstalls(h hash, libName string) []string {
	return installs
}

// InstallPaths returns the ordered channel of indexed install paths.
func (ni *NoticeIndex) InstallPaths() chan string {
	c := make(chan string)
	go func() {
		paths := make([]string, 0, len(ni.installHashLib))
		for path := range ni.installHashLib {
			paths = append(paths, path)
		}
		sort.Strings(paths)
		for _, installPath := range paths {
			c <- installPath
		}
		close(c)
	}()
	return c
}

// InstallHashes returns the ordered array of hashes attached to `installPath`.
func (ni *NoticeIndex) InstallHashes(installPath string) []hash {
	result := make([]hash, 0, len(ni.installHashLib[installPath]))
	for h := range ni.installHashLib[installPath] {
		result = append(result, h)
	}
	if len(result) > 0 {
		sort.Sort(hashList{ni, "", installPath, &result})
	}
	return result
}

// InstallHashLibs returns the ordered array of library names attached to
// `installPath` as hash `h`.
func (ni *NoticeIndex) InstallHashLibs(installPath string, h hash) []string {
	result := make([]string, 0, len(ni.installHashLib[installPath][h]))
	for libName := range ni.installHashLib[installPath][h] {
		result = append(result, libName)
	}
	sort.Strings(result)
	return result
}

// HashText returns the file content of the license text hashed as `h`.
func (ni *NoticeIndex) HashText(h hash) []byte {
	return ni.text[h]
@@ -494,6 +534,7 @@ func (h hash) String() string {
type hashList struct {
	ni          *NoticeIndex
	libName     string
	installPath string
	hashes      *[]hash
}

@@ -511,6 +552,14 @@ func (l hashList) Less(i, j int) bool {
	if 0 < len(l.libName) {
		insti = len(l.ni.hashLibInstall[(*l.hashes)[i]][l.libName])
		instj = len(l.ni.hashLibInstall[(*l.hashes)[j]][l.libName])
	} else {
		libsi := l.ni.InstallHashLibs(l.installPath, (*l.hashes)[i])
		libsj := l.ni.InstallHashLibs(l.installPath, (*l.hashes)[j])
		libsis := strings.Join(libsi, " ")
		libsjs := strings.Join(libsj, " ")
		if libsis != libsjs {
			return libsis < libsjs
		}
	}
	if insti == instj {
		leni := len(l.ni.text[(*l.hashes)[i]])