You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
280 lines
8.8 KiB
280 lines
8.8 KiB
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/internal/pathrule"
|
|
)
|
|
|
|
// The labels of spec §29.7, which the official SDK must use.
|
|
const (
|
|
authorLabel = "autor declarado (texto del creador, sin comprobar):"
|
|
commentTitle = "Comentario del creador (sin comprobar)"
|
|
noteTitle = "Nota pública del creador (sin comprobar)"
|
|
// prefix goes before every piece of text of the creator. It counts 3
|
|
// columns: U+2502 is of ambiguous width, 2 columns in a CJK terminal.
|
|
prefix = "│ "
|
|
prefixWidth = 3
|
|
// defaultWidth is W when the output is not a terminal, and minWidth the
|
|
// least W ever used.
|
|
defaultWidth = 80
|
|
minWidth = 20
|
|
// contMark goes before each row of a line of the verdicts after its first.
|
|
// It counts 5 columns: U+21B3, like any code point that is not ASCII,
|
|
// counts 2.
|
|
contMark = " ↳ "
|
|
contMarkWidth = 5
|
|
// unusableNote is what a reader says of a public note that breaks the
|
|
// rules of text, which it does not show (spec v0.11, §24.1).
|
|
unusableNote = " La cápsula lleva una nota pública que no cumple las reglas de texto: no se muestra."
|
|
)
|
|
|
|
// outputWidth is W for w (spec §29.7): the width of the terminal, or 80 when
|
|
// w is not one, and never less than 20.
|
|
func outputWidth(w io.Writer) int {
|
|
width := defaultWidth
|
|
if f, ok := w.(*os.File); ok {
|
|
if n, ok := termWidth(f); ok && n > 0 {
|
|
width = n
|
|
}
|
|
}
|
|
return max(width, minWidth)
|
|
}
|
|
|
|
// runeWidth counts the width of r by excess: 1 for printable ASCII, 2 for
|
|
// any other code point (spec §29.7).
|
|
func runeWidth(r rune) int {
|
|
if r >= 0x20 && r <= 0x7E {
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
|
|
// expandTabs turns each TAB of line into spaces up to the next column that
|
|
// is a multiple of 8, counting columns as runeWidth does.
|
|
func expandTabs(line string) string {
|
|
if !strings.Contains(line, "\t") {
|
|
return line
|
|
}
|
|
var b strings.Builder
|
|
col := 0
|
|
for _, r := range line {
|
|
if r == '\t' {
|
|
n := 8 - col%8
|
|
b.WriteString(strings.Repeat(" ", n))
|
|
col += n
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
col += runeWidth(r)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// pieces splits line into pieces of at most limit columns, each with at
|
|
// least one code point. An empty line is one empty piece.
|
|
func pieces(line string, limit int) []string {
|
|
var out []string
|
|
start, width := 0, 0
|
|
for i, r := range line {
|
|
w := runeWidth(r)
|
|
if width+w > limit && i > start {
|
|
out = append(out, line[start:i])
|
|
start, width = i, 0
|
|
}
|
|
width += w
|
|
}
|
|
return append(out, line[start:])
|
|
}
|
|
|
|
// writeCreator writes text of the creator, line by line, each line in
|
|
// pieces of at most W - 3 columns behind the prefix: no line of the creator
|
|
// is ever broken by the terminal or shown without the prefix, so none can
|
|
// pass for a line of the reader (spec §29.7).
|
|
func writeCreator(w io.Writer, text string, width int) {
|
|
for _, line := range strings.Split(text, "\n") {
|
|
for _, p := range pieces(expandTabs(line), width-prefixWidth) {
|
|
fmt.Fprintln(w, prefix+p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// writeVerdicts writes lines of the reader, such as the verdicts, each in
|
|
// rows of at most W - 3 columns, and each row after the first behind
|
|
// contMark. The terminal never breaks one of them on its own, so no text that
|
|
// a certificate gives, inside a line, can start a row and pass for a verdict
|
|
// (spec §29.7).
|
|
func writeVerdicts(w io.Writer, lines []string, width int) {
|
|
for _, line := range lines {
|
|
for i, row := range rows(line, width-prefixWidth, width-prefixWidth-contMarkWidth) {
|
|
if i > 0 {
|
|
row = contMark + row
|
|
}
|
|
fmt.Fprintln(w, row)
|
|
}
|
|
}
|
|
}
|
|
|
|
// rows splits line into rows of at most first columns, the first one, and
|
|
// rest columns, the others. A row ends at the last space that fits after
|
|
// some text, and the break drops that space; only a word longer than a row
|
|
// is broken inside, after the last code point that fits.
|
|
func rows(line string, first, rest int) []string {
|
|
var out []string
|
|
limit := first
|
|
for {
|
|
end, width, lastSpace, text := len(line), 0, -1, false
|
|
for i, r := range line {
|
|
w := runeWidth(r)
|
|
if width+w > limit && i > 0 {
|
|
end = i
|
|
break
|
|
}
|
|
if r == ' ' && text {
|
|
lastSpace = i
|
|
}
|
|
text = text || r != ' '
|
|
width += w
|
|
}
|
|
if end == len(line) {
|
|
return append(out, line)
|
|
}
|
|
cut, next := end, end
|
|
switch {
|
|
case line[end] == ' ':
|
|
next = end + 1
|
|
case lastSpace > 0:
|
|
cut, next = lastSpace, lastSpace+1
|
|
}
|
|
out = append(out, line[:cut])
|
|
line, limit = line[next:], rest
|
|
}
|
|
}
|
|
|
|
// present shows what a format 3 capsule holds, after step 18, as spec §29.7
|
|
// says: the verdicts first, then the declared author and the comment as
|
|
// text of the creator that nobody has checked, then the paths of the files
|
|
// written to dir, with a warning after those that are risky to open, and the
|
|
// verdicts again at the end.
|
|
func present(w io.Writer, o *capsule.Opened, dir string, width int) {
|
|
verdicts := o.Verdicts.Lines()
|
|
writeVerdicts(w, verdicts, width)
|
|
h := o.Head
|
|
// Spec v0.11 §29.7: under a valid seal, a modification time later than the
|
|
// instant of the seal is shown as an inconsistency.
|
|
if t, ok := o.Verdicts.SealedAt(); ok {
|
|
for _, f := range h.Files {
|
|
if f.HasMTime && time.Unix(int64(f.MTime), 0).After(t) {
|
|
writeVerdicts(w, []string{fmt.Sprintf(" aviso: la fecha de modificación de un fichero es posterior al sello (%s): no es coherente.", t.UTC().Format(time.RFC3339))}, width)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if o.Inspection != nil && o.Inspection.Header != nil {
|
|
if note, ok := o.Inspection.Header.PublicNote(); ok {
|
|
fmt.Fprintln(w, "┌ "+noteTitle)
|
|
writeCreator(w, note, width)
|
|
fmt.Fprintln(w, "└")
|
|
} else if o.Inspection.Header.UnusableNote() {
|
|
writeVerdicts(w, []string{unusableNote}, width)
|
|
}
|
|
}
|
|
if h.Author != "" {
|
|
fmt.Fprintln(w, authorLabel)
|
|
writeCreator(w, h.Author, width)
|
|
}
|
|
if h.Comment != "" {
|
|
fmt.Fprintln(w, "┌ "+commentTitle)
|
|
writeCreator(w, h.Comment, width)
|
|
fmt.Fprintln(w, "└")
|
|
}
|
|
if len(h.Files) > 0 {
|
|
fmt.Fprintf(w, "Ficheros escritos en %s (%d):\n", dir, len(h.Files))
|
|
for _, f := range h.Files {
|
|
writeCreator(w, f.Path, width)
|
|
for _, warning := range risks(f.Path) {
|
|
fmt.Fprintln(w, " aviso: "+warning)
|
|
}
|
|
}
|
|
}
|
|
writeVerdicts(w, verdicts, width)
|
|
}
|
|
|
|
// The names that spec §29.7 asks a reader to warn of, compared by their key
|
|
// of R7, so that ".GIT" or "Informe.LNK" warn too.
|
|
var (
|
|
shortcutExts = keys(".lnk", ".url", ".library-ms", ".searchConnector-ms")
|
|
programExts = keys(".exe", ".com", ".bat", ".cmd", ".scr", ".pif", ".msi", ".msp", ".cpl", ".hta",
|
|
".jar", ".js", ".jse", ".vbs", ".vbe", ".wsf", ".wsh", ".ps1", ".psm1", ".reg", ".sh", ".command", ".app")
|
|
desktopINI = pathrule.Key("desktop.ini")
|
|
gitDir = pathrule.Key(".git")
|
|
)
|
|
|
|
func keys(names ...string) map[string]bool {
|
|
m := make(map[string]bool, len(names))
|
|
for _, n := range names {
|
|
m[pathrule.Key(n)] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
// risks returns the warnings for path: a Windows shortcut or folder
|
|
// setting, a .git folder, a program, or a segment that starts with '-'.
|
|
func risks(path string) []string {
|
|
var out []string
|
|
segs := strings.Split(path, "/")
|
|
for i, s := range segs {
|
|
k := pathrule.Key(s)
|
|
switch {
|
|
case k == gitDir && i < len(segs)-1:
|
|
out = append(out, "está dentro de una carpeta .git, cuyos ganchos pueden ejecutar órdenes")
|
|
case k == gitDir:
|
|
out = append(out, "se llama .git y puede apuntar a otro repositorio")
|
|
case k == desktopINI:
|
|
out = append(out, "es la configuración de una carpeta de Windows")
|
|
}
|
|
if strings.HasPrefix(s, "-") {
|
|
out = append(out, "un nombre que empieza por '-' puede tomarse por una opción en una orden")
|
|
}
|
|
}
|
|
last := segs[len(segs)-1]
|
|
if dot := strings.LastIndexByte(last, '.'); dot > 0 {
|
|
switch ext := pathrule.Key(last[dot:]); {
|
|
case shortcutExts[ext]:
|
|
out = append(out, "es un acceso directo de Windows: puede abrir otro programa o una dirección")
|
|
case programExts[ext]:
|
|
out = append(out, "es un programa o un script: no lo ejecutes sin saber qué hace")
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// showNote shows the public note of an inspected capsule as spec v0.11 §24.1
|
|
// and §29.7 ask: as text of the creator that nobody has checked, with the
|
|
// prefix and the wrapping of any text of the creator, and, before the date,
|
|
// the warning that nobody can check who made the capsule or whether it is
|
|
// signed. A note that breaks the rules of text is not shown, and the person
|
|
// is told. It shows the note even when the capsule fails a check: that is
|
|
// when a forged one is most likely.
|
|
func showNote(w io.Writer, in *capsule.Inspection) {
|
|
if in == nil || in.Header == nil {
|
|
return
|
|
}
|
|
note, ok := in.Header.PublicNote()
|
|
if !ok {
|
|
if in.Header.UnusableNote() {
|
|
fmt.Fprintln(w, unusableNote)
|
|
}
|
|
return
|
|
}
|
|
fmt.Fprintln(w, "┌ "+noteTitle)
|
|
writeCreator(w, note, outputWidth(w))
|
|
fmt.Fprintln(w, "└")
|
|
fmt.Fprintln(w, " Nadie puede comprobar antes de la fecha quién creó la cápsula ni si va firmada.")
|
|
}
|