diff options
Diffstat (limited to 'internal/slackware_com/changelog.go')
-rw-r--r-- | internal/slackware_com/changelog.go | 428 |
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 + +} |