aboutsummaryrefslogtreecommitdiff
path: root/internal/slackware_com/changelog.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/slackware_com/changelog.go')
-rw-r--r--internal/slackware_com/changelog.go428
1 files changed, 428 insertions, 0 deletions
diff --git a/internal/slackware_com/changelog.go b/internal/slackware_com/changelog.go
new file mode 100644
index 0000000..90ff02b
--- /dev/null
+++ b/internal/slackware_com/changelog.go
@@ -0,0 +1,428 @@
+package slackware_com
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "strings"
+ "time"
+)
+
+const ChangeLogTxt = "ChangeLog.txt"
+
+const Break = "+--------------------------+"
+
+// The format is randomly used on Fri 01 Feb 2019 01:26:44 AM UTC.
+const AltTimeFormat = "Mon _2 Jan 2006 03:04:05 PM MST"
+
+type EntryItem interface {
+ ChangeLogEntry()
+ String() string
+}
+
+type PathUpdate interface {
+ GetPath() string
+}
+
+type EndOfLifeNotice string
+
+func (e EndOfLifeNotice) ChangeLogEntry() {
+}
+
+func (e EndOfLifeNotice) String() string {
+ return string(e)
+}
+
+type PathAdded struct {
+ Path string
+ Description string
+}
+
+func (e *PathAdded) ChangeLogEntry() {
+}
+
+func (e *PathAdded) GetPath() string {
+ return e.Path
+}
+
+func (e *PathAdded) String() string {
+ str := fmt.Sprintf("%s: Added.", e.Path)
+ if e.Description != "" {
+ str += "\n" + e.Description
+ }
+ return str
+}
+
+type PathRebuild struct {
+ Path string
+ Description string
+}
+
+func (e *PathRebuild) ChangeLogEntry() {
+}
+
+func (e *PathRebuild) GetPath() string {
+ return e.Path
+}
+
+func (e *PathRebuild) String() string {
+ str := fmt.Sprintf("%s: Rebuilt.", e.Path)
+ if e.Description != "" {
+ str += "\n" + e.Description
+ }
+ return str
+}
+
+type PathRemoved struct {
+ Path string
+ Description string
+}
+
+func (e *PathRemoved) ChangeLogEntry() {
+}
+
+func (e *PathRemoved) GetPath() string {
+ return e.Path
+}
+
+func (e *PathRemoved) String() string {
+ str := fmt.Sprintf("%s: Removed.", e.Path)
+ if e.Description != "" {
+ str += "\n" + e.Description
+ }
+ return str
+}
+
+type PathUpgraded struct {
+ Path string
+ Description string
+}
+
+func (e *PathUpgraded) ChangeLogEntry() {
+}
+
+func (e *PathUpgraded) GetPath() string {
+ return e.Path
+}
+
+func (e *PathUpgraded) String() string {
+ str := fmt.Sprintf("%s: Upgraded.", e.Path)
+ if e.Description != "" {
+ str += "\n" + e.Description
+ }
+ return str
+}
+
+type Unknown string
+
+func (e Unknown) ChangeLogEntry() {
+}
+
+func (e Unknown) String() string {
+ return string(e)
+}
+
+// Does the file path match the give path.
+//
+// Slackware's ChangeLog sometimes includes paths with '*' patterns in them,
+// for example 'patches/packages/linux-5.15.117/*: Upgraded.'.
+func IsPathMatch(pattern string, filePath string) bool {
+ if !strings.Contains(pattern, "*") {
+ return pattern == filePath
+ }
+
+ for {
+ if pattern == "" && filePath == "" {
+ return true
+ }
+
+ pDir, pFile := path.Split(pattern)
+ dir, file := path.Split(filePath)
+
+ if pFile != "*" || pFile != file {
+ return false
+ }
+
+ pattern = pDir
+ filePath = dir
+ }
+}
+
+func FindMatches(pattern string, filePaths []string) []string {
+ var matched []string
+
+ for _, filePath := range filePaths {
+ if IsPathMatch(pattern, filePath) {
+ matched = append(matched, filePath)
+ }
+
+ }
+
+ return matched
+}
+
+func isPathUpdateLine(line string) bool {
+ _, updateType, ok := strings.Cut(line, ":")
+ if !ok {
+ return false
+ }
+
+ switch strings.TrimSpace(updateType) {
+ case "Added.",
+ "Upgraded.",
+ "Rebuilt.",
+ "Removed.":
+ return true
+ default:
+ return false
+ }
+}
+
+func addIndentation(str string) string {
+ lines := strings.Split(str, "\n")
+ for i := range lines {
+ lines[i] = " " + lines[i]
+ }
+ return strings.Join(lines, "\n")
+}
+
+func removeIndentation(str string) string {
+ lines := strings.Split(str, "\n")
+ for i := range lines {
+ lines[i] = strings.TrimPrefix(lines[i], " ")
+ }
+ return strings.Join(lines, "\n")
+}
+
+func ParseChangeLogEntry(arg string) EntryItem {
+ packageLine, tail, _ := strings.Cut(arg, "\n")
+
+ path, updateType, ok := strings.Cut(packageLine, ":")
+ if ok {
+ switch strings.TrimSpace(updateType) {
+ case "Added.":
+ return &PathAdded{
+ Path: path,
+ Description: removeIndentation(tail),
+ }
+ case "Upgraded.":
+ return &PathUpgraded{
+ Path: path,
+ Description: removeIndentation(tail),
+ }
+ case "Rebuilt.":
+ return &PathRebuild{
+ Path: path,
+ Description: removeIndentation(tail),
+ }
+ case "Removed.":
+ return &PathRemoved{
+ Path: path,
+ Description: removeIndentation(tail),
+ }
+ }
+ }
+
+ if strings.Contains(arg, "END OF LIFE") {
+ return EndOfLifeNotice(arg)
+ }
+
+ return Unknown(arg)
+}
+
+type Entry struct {
+ Items []EntryItem
+ Time time.Time
+}
+
+func (d *Entry) UnmarshalText(text []byte) error {
+ date, str, ok := strings.Cut(string(text), "\n")
+ if !ok {
+ return errors.New("first line must be a date")
+ }
+
+ var err error
+ d.Time, err = time.Parse(time.UnixDate, date)
+ if err != nil {
+ unixDateErr := err
+
+ d.Time, err = time.Parse(AltTimeFormat, date)
+ if err != nil {
+ return fmt.Errorf("date line: %w", unixDateErr)
+ }
+
+ }
+
+ line, str, _ := strings.Cut(str, "\n")
+ for line != "" {
+ entryStr := line
+ for {
+ line, str, _ = strings.Cut(str, "\n")
+ if line == "" || isPathUpdateLine(line) {
+ break
+ }
+
+ entryStr += "\n" + line
+ }
+
+ item := ParseChangeLogEntry(entryStr)
+ d.Items = append(d.Items, item)
+ }
+
+ return nil
+}
+
+func (d *Entry) String() string {
+ str := d.Time.Format(time.UnixDate)
+ for _, item := range d.Items {
+ str += "\n" + addIndentation(item.String())
+ }
+ return str
+}
+
+type ChangeLog struct {
+ Entries []*Entry
+}
+
+func OpenChangeLog(filepath string) (*ChangeLog, error) {
+ f, err := os.Open(filepath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ buf, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+
+ var changeLog ChangeLog
+ err = changeLog.UnmarshalText(buf)
+ if err != nil {
+ return nil, err
+ }
+
+ return &changeLog, nil
+}
+
+func (c *ChangeLog) UnmarshalText(buf []byte) error {
+ buffer := bytes.NewBuffer(buf)
+ r := bufio.NewReader(buffer)
+
+mainLoop:
+ for {
+ var dayStr string
+ for {
+ line, _, err := r.ReadLine()
+ if err == io.EOF {
+ break mainLoop
+ } else if err != nil {
+ return err
+ }
+ if string(line) == Break {
+ break
+ }
+
+ if dayStr == "" {
+ dayStr = string(line)
+ } else {
+ dayStr = dayStr + "\n" + string(line)
+ }
+ }
+
+ var entry Entry
+ err := entry.UnmarshalText([]byte(dayStr))
+ if err != nil {
+ return err
+ }
+
+ c.Entries = append(c.Entries, &entry)
+ }
+
+ return nil
+}
+
+func (c *ChangeLog) First(pred func(EntryItem) bool) (*Entry, bool) {
+ for _, entry := range c.Entries {
+ for _, item := range entry.Items {
+ if pred(item) {
+ return &Entry{
+ Items: []EntryItem{item},
+ Time: entry.Time,
+ }, true
+ }
+ }
+ }
+
+ return nil, false
+}
+
+func (c *ChangeLog) Select(pred func(EntryItem) bool) []Entry {
+ var entries []Entry
+
+ for _, entry := range c.Entries {
+ var items []EntryItem
+
+ for _, item := range entry.Items {
+ if pred(item) {
+ item := item
+ items = append(items, item)
+ }
+ }
+
+ if len(items) > 0 {
+ entries = append(entries, Entry{
+ Items: items,
+ Time: entry.Time,
+ })
+ }
+ }
+
+ return entries
+}
+
+func GetLastInstallTime(
+ changeLog *ChangeLog,
+ installed []PackageName,
+ files []string,
+) time.Time {
+ isInstalled := make(map[string]bool)
+ for _, pkg := range installed {
+ isInstalled[pkg.String()] = true
+ }
+
+ entry, ok := changeLog.First(func(item EntryItem) bool {
+ pathItem, ok := item.(PathUpdate)
+ if !ok {
+ return false
+ }
+
+ filePaths := []string{pathItem.GetPath()}
+ if strings.Contains(filePaths[0], "*") {
+ filePaths = FindMatches(pathItem.GetPath(), files)
+ }
+
+ for _, fp := range filePaths {
+ pkg, err := NewPackageNameFromPath(fp)
+ if err != nil {
+ continue
+ }
+
+ if isInstalled[pkg.String()] {
+ return true
+ }
+ }
+
+ return false
+ })
+ if !ok {
+ return time.Time{}
+ }
+
+ return entry.Time
+
+}