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.
214 lines
6.6 KiB
214 lines
6.6 KiB
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/extension"
|
|
"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
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
for _, line := range verdicts {
|
|
fmt.Fprintln(w, line)
|
|
}
|
|
h := o.Head
|
|
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, "└")
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
for _, line := range verdicts {
|
|
fmt.Fprintln(w, line)
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
for _, e := range in.Header.Noncritical {
|
|
if e.ID == extension.NoteID && e.Version == 1 {
|
|
fmt.Fprintln(w, " La cápsula lleva una nota pública que no cumple las reglas de texto: no se muestra.")
|
|
}
|
|
}
|
|
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.")
|
|
}
|