From 55f3590510073e402c9ba8ab234c13d56b082535 Mon Sep 17 00:00:00 2001 From: mr-pmillz <> Date: Wed, 4 Sep 2024 23:53:45 -0400 Subject: [PATCH] optimized code, dynamic table columns --- cmd/eoldate/main.go | 334 ++++++++++++++++++++++++++++++++------------ eoldate.go | 24 ++-- 2 files changed, 254 insertions(+), 104 deletions(-) diff --git a/cmd/eoldate/main.go b/cmd/eoldate/main.go index 5855801..fdde38b 100644 --- a/cmd/eoldate/main.go +++ b/cmd/eoldate/main.go @@ -3,13 +3,13 @@ package main import ( "flag" "fmt" - "github.com/mr-pmillz/eoldate" - "github.com/olekukonko/tablewriter" - "github.com/projectdiscovery/gologger" "os" - "reflect" "strings" "time" + + "github.com/mr-pmillz/eoldate" + "github.com/olekukonko/tablewriter" + "github.com/projectdiscovery/gologger" ) func main() { @@ -19,9 +19,20 @@ func main() { getAll := flag.Bool("getall", false, "get all results from all technologies") flag.Parse() - eolOptions := eoldate.Options{} - if *output != "" { - absOutputDir, err := eoldate.ResolveAbsPath(*output) + eolOptions := eoldate.Options{ + Tech: *tech, + Output: *output, + Version: *version, + GetAll: *getAll, + } + + if eolOptions.Version { + fmt.Printf("Version: %s\n", eoldate.CurrentVersion) + os.Exit(0) + } + + if eolOptions.Output != "" { + absOutputDir, err := eoldate.ResolveAbsPath(eolOptions.Output) if err != nil { gologger.Fatal().Msg(err.Error()) } @@ -31,18 +42,9 @@ func main() { } } - eolOptions.Tech = *tech - eolOptions.Version = *version - eolOptions.GetAll = *getAll - - if eolOptions.Version { - fmt.Printf("Version: %s\n", eoldate.CurrentVersion) - os.Exit(0) - } - - client := eoldate.NewClient(eoldate.EOLBaseURL) + client := eoldate.NewClient() - if *getAll { + if eolOptions.GetAll { gologger.Info().Msg("Getting all available technologies") allProducts, err := client.GetAllProducts() if err != nil { @@ -57,101 +59,247 @@ func main() { flag.Usage() os.Exit(1) } + releaseVersions, err := client.GetProduct(eolOptions.Tech) if err != nil { - fmt.Printf("Error fetching product data: %v\n", err) - return + gologger.Fatal().Msgf("Error fetching product data: %v", err) } - // Create a new table + + headers, includeFlags := determineHeaders(releaseVersions) tableString := &strings.Builder{} - table := tablewriter.NewWriter(tableString) - table.SetHeader([]string{"Cycle", "Release Date", "EOL Date", "Latest", "Link", "Latest Release Date", "LTS", "Support", "Extended Support"}) - - table.SetHeaderColor( - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiYellowColor, tablewriter.BgBlackColor}, - ) - // Get current date + table := createTable(tableString, headers) + currentDate := time.Now() - row := make([]string, 0) - var eolDateTime time.Time - // Add data to the table for _, release := range releaseVersions { - var EOLIsBool bool - rt := reflect.TypeOf(release.EOL) - if rt.Kind() == reflect.String { - eolDateTime, err = time.Parse("2006-01-02", release.EOL.(string)) - if err != nil { - fmt.Println("Error parsing date:", err) - continue - } - row = []string{release.Cycle, release.ReleaseDate, release.EOL.(string), release.Latest, release.Link, release.LatestReleaseDate, fmt.Sprintf("%s", release.LTS), fmt.Sprintf("%s", release.Support), fmt.Sprintf("%s", release.ExtendedSupport)} + row := buildRow(release, includeFlags) + colorRow(table, row, release.EOL, release.Support, currentDate) + } + + setTableProperties(table, eolOptions.Tech, len(headers)) // Pass the number of columns + table.Render() + fmt.Println(tableString.String()) + + if eolOptions.Output != "" { + writeOutputFiles(eolOptions, tableString.String(), releaseVersions) + } +} + +func determineHeaders(releases []eoldate.Product) ([]string, map[string]bool) { + headers := []string{"Cycle", "Release Date", "EOL Date", "Latest"} + includeFlags := map[string]bool{ + "Link": false, + "LatestReleaseDate": false, + "LTS": false, + "Support": false, + "ExtendedSupport": false, + "MinJavaVersion": false, + "SupportedPHPVersions": false, + } + + for _, release := range releases { + if release.Link != "" { + includeFlags["Link"] = true } - if rt.Kind() == reflect.Bool { - EOLIsBool = true - row = []string{release.Cycle, release.ReleaseDate, "N/A", release.Latest, release.Link, release.LatestReleaseDate, fmt.Sprintf("%s", release.LTS), fmt.Sprintf("%s", release.Support), fmt.Sprintf("%s", release.ExtendedSupport)} + if release.LatestReleaseDate != "" { + includeFlags["LatestReleaseDate"] = true } - - // Check if EOL date is older or later than the current date - if eolDateTime.Before(currentDate) && !EOLIsBool { - table.Rich(row, []tablewriter.Colors{{}, {}, {tablewriter.FgRedColor}, {}, {}, {}, {}}) - } else { - table.Rich(row, []tablewriter.Colors{{}, {}, {tablewriter.FgGreenColor}, {}, {}, {}, {}}) + if release.LTS != nil { + includeFlags["LTS"] = true + } + if release.Support != nil { + includeFlags["Support"] = true + } + if release.ExtendedSupport != nil { + includeFlags["ExtendedSupport"] = true + } + if release.MinJavaVersion != nil { + includeFlags["MinJavaVersion"] = true + } + if release.SupportedPHPVersions != "" { + includeFlags["SupportedPHPVersions"] = true } } - table.SetColumnAlignment([]int{ - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - tablewriter.ALIGN_CENTER, - }) + // Add headers in the desired order + if includeFlags["Link"] { + headers = append(headers, "Link") + } + if includeFlags["LatestReleaseDate"] { + headers = append(headers, "Latest Release Date") + } + if includeFlags["LTS"] { + headers = append(headers, "LTS") + } + if includeFlags["Support"] { + headers = append(headers, "Support") + } + if includeFlags["ExtendedSupport"] { + headers = append(headers, "Extended Support") + } + if includeFlags["MinJavaVersion"] { + headers = append(headers, "Min Java Version") + } + if includeFlags["SupportedPHPVersions"] { + headers = append(headers, "Supported PHP Versions") + } + + return headers, includeFlags +} +func createTable(writer *strings.Builder, headers []string) *tablewriter.Table { + table := tablewriter.NewWriter(writer) + table.SetHeader(headers) table.SetAutoWrapText(true) table.SetRowLine(true) - table.SetFooter([]string{ - strings.ToUpper(eolOptions.Tech), - "", - "", - "", - "", - "", - "", - "", - "", - }) - table.SetFooterAlignment(tablewriter.ALIGN_LEFT) + alignments := make([]int, len(headers)) + for i := range alignments { + alignments[i] = tablewriter.ALIGN_CENTER + } + table.SetColumnAlignment(alignments) + return table +} - // Render the table - table.Render() - fmt.Println(tableString.String()) +func buildRow(release eoldate.Product, includeFlags map[string]bool) []string { + row := []string{release.Cycle, release.ReleaseDate, formatEOL(release.EOL), release.Latest} - if eolOptions.Output != "" { - tableOutputFile := fmt.Sprintf("%s/%s.txt", eolOptions.Output, eolOptions.Tech) - softwareEolDateCSV := fmt.Sprintf("%s/%s.csv", eolOptions.Output, eolOptions.Tech) - softwareEolDateJSON := fmt.Sprintf("%s/%s.json", eolOptions.Output, eolOptions.Tech) + if includeFlags["Link"] { + row = append(row, release.Link) + } + if includeFlags["LatestReleaseDate"] { + row = append(row, release.LatestReleaseDate) + } + if includeFlags["LTS"] { + row = append(row, formatBool(release.LTS)) + } + if includeFlags["Support"] { + row = append(row, formatInterface(release.Support)) + } + if includeFlags["ExtendedSupport"] { + row = append(row, formatInterface(release.ExtendedSupport)) + } + if includeFlags["MinJavaVersion"] && release.MinJavaVersion != nil { + row = append(row, formatJavaVersion(*release.MinJavaVersion)) + } + if includeFlags["SupportedPHPVersions"] { + row = append(row, release.SupportedPHPVersions) + } - if err = eoldate.WriteStringToFile(tableOutputFile, tableString.String()); err != nil { - gologger.Fatal().Msg(err.Error()) + return row +} + +func formatEOL(eol interface{}) string { + switch v := eol.(type) { + case string: + return v + case bool: + return fmt.Sprintf("%t", v) + default: + return "N/A" + } +} + +func formatBool(value interface{}) string { + if v, ok := value.(bool); ok { + return fmt.Sprintf("%t", v) + } + return "N/A" +} + +func formatInterface(value interface{}) string { + if value == nil { + return "N/A" + } + return fmt.Sprintf("%v", value) +} + +func colorRow(table *tablewriter.Table, row []string, eol interface{}, support interface{}, currentDate time.Time) { + eolDate, eolErr := parseEOLDate(eol) + supportDate, supportErr := parseEOLDate(support) + + colors := make([]tablewriter.Colors, len(row)) + for i := range colors { + colors[i] = tablewriter.Colors{} + } + + if eolErr == nil && !eolDate.IsZero() { + if eolDate.Before(currentDate) { + colors[2] = tablewriter.Colors{tablewriter.FgRedColor} + } else { + colors[2] = tablewriter.Colors{tablewriter.FgGreenColor} } - if err = eoldate.WriteStructToJSONFile(releaseVersions, softwareEolDateJSON); err != nil { - gologger.Fatal().Msg(err.Error()) + } + + supportIndex := -1 + for i, val := range row { + if val == formatInterface(support) { + supportIndex = i + break } - if err = eoldate.WriteStructToCSVFile(releaseVersions, softwareEolDateCSV); err != nil { - gologger.Fatal().Msg(err.Error()) + } + + if supportIndex != -1 && supportErr == nil && !supportDate.IsZero() { + if supportDate.Before(currentDate) { + colors[supportIndex] = tablewriter.Colors{tablewriter.FgRedColor} + } else { + colors[supportIndex] = tablewriter.Colors{tablewriter.FgGreenColor} + } + } + + table.Rich(row, colors) +} + +func parseEOLDate(date interface{}) (time.Time, error) { + if dateStr, ok := date.(string); ok { + layouts := []string{ + "2006-01-02", + "2006-01", + "2006", + } + for _, layout := range layouts { + if t, err := time.Parse(layout, dateStr); err == nil { + return t, nil + } + } + } + return time.Time{}, fmt.Errorf("invalid date format") +} + +// setTableProperties ... +func setTableProperties(table *tablewriter.Table, tech string, columnCount int) { + footer := make([]string, columnCount) + footer[0] = strings.ToUpper(tech) + for i := 1; i < columnCount; i++ { + footer[i] = "" + } + table.SetFooter(footer) + table.SetFooterAlignment(tablewriter.ALIGN_LEFT) +} + +// Add this new function to format the Java version +func formatJavaVersion(version float64) string { + if version == float64(int(version)) { + return fmt.Sprintf("%.0f", version) + } + return fmt.Sprintf("%.1f", version) +} + +func writeOutputFiles(options eoldate.Options, tableString string, releaseVersions []eoldate.Product) { + files := map[string]func() error{ + fmt.Sprintf("%s/%s.txt", options.Output, options.Tech): func() error { + return eoldate.WriteStringToFile(fmt.Sprintf("%s/%s.txt", options.Output, options.Tech), tableString) + }, + fmt.Sprintf("%s/%s.json", options.Output, options.Tech): func() error { + return eoldate.WriteStructToJSONFile(releaseVersions, fmt.Sprintf("%s/%s.json", options.Output, options.Tech)) + }, + fmt.Sprintf("%s/%s.csv", options.Output, options.Tech): func() error { + return eoldate.WriteStructToCSVFile(releaseVersions, fmt.Sprintf("%s/%s.csv", options.Output, options.Tech)) + }, + } + + for filename, writeFunc := range files { + if err := writeFunc(); err != nil { + gologger.Error().Msgf("Failed to write %s: %v", filename, err) } } } diff --git a/eoldate.go b/eoldate.go index 6786d9c..adc0d2a 100644 --- a/eoldate.go +++ b/eoldate.go @@ -20,15 +20,17 @@ type Options struct { // Product represents the structure of the JSON data type Product struct { - Cycle string `json:"cycle,omitempty"` - ReleaseDate string `json:"releaseDate,omitempty"` - EOL interface{} `json:"eol,omitempty"` - Latest string `json:"latest,omitempty"` - Link string `json:"link,omitempty"` - LatestReleaseDate string `json:"latestReleaseDate,omitempty"` - LTS interface{} `json:"lts,omitempty"` - Support interface{} `json:"support,omitempty"` - ExtendedSupport interface{} `json:"extendedSupport,omitempty"` + Cycle string `json:"cycle,omitempty"` + ReleaseDate string `json:"releaseDate,omitempty"` + EOL interface{} `json:"eol,omitempty"` + Latest string `json:"latest,omitempty"` + Link string `json:"link,omitempty"` + LatestReleaseDate string `json:"latestReleaseDate,omitempty"` + LTS interface{} `json:"lts,omitempty"` + Support interface{} `json:"support,omitempty"` + ExtendedSupport interface{} `json:"extendedSupport,omitempty"` + MinJavaVersion *float64 `json:"minJavaVersion,omitempty"` + SupportedPHPVersions string `json:"supportedPHPVersions,omitempty"` } type AllProducts []string @@ -40,10 +42,10 @@ type Client struct { } // NewClient creates a new API client with the given base URL. -func NewClient(baseURL string) *Client { +func NewClient() *Client { return &Client{ httpClient: &http.Client{}, - baseURL: baseURL, + baseURL: EOLBaseURL, } }