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 }