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.") }