diff --git a/internal/googlereader/handler.go b/internal/googlereader/handler.go index 84758349..7f0753f4 100644 --- a/internal/googlereader/handler.go +++ b/internal/googlereader/handler.go @@ -60,8 +60,6 @@ const ( BroadcastFriends = "broadcast-friends" // Like is the suffix for like stream Like = "like" - // EntryIDLong is the long entry id representation - EntryIDLong = "tag:google.com,2005:reader/item/%016x" ) const ( @@ -370,28 +368,6 @@ func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType return tags, nil } -func getItemIDs(r *http.Request) ([]int64, error) { - items := r.Form[ParamItemIDs] - if len(items) == 0 { - return nil, fmt.Errorf("googlereader: no items requested") - } - - itemIDs := make([]int64, len(items)) - - for i, item := range items { - var itemID int64 - _, err := fmt.Sscanf(item, EntryIDLong, &itemID) - if err != nil { - itemID, err = strconv.ParseInt(item, 16, 64) - if err != nil { - return nil, fmt.Errorf("googlereader: could not parse item: %v", item) - } - } - itemIDs[i] = itemID - } - return itemIDs, nil -} - func checkOutputFormat(r *http.Request) error { var output string if r.Method == http.MethodPost { @@ -568,7 +544,7 @@ func (h *handler) editTagHandler(w http.ResponseWriter, r *http.Request) { return } - itemIDs, err := getItemIDs(r) + itemIDs, err := parseItemIDsFromRequest(r) if err != nil { json.ServerError(w, r, err) return @@ -985,7 +961,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque userRead := fmt.Sprintf(UserStreamPrefix, userID) + Read userStarred := fmt.Sprintf(UserStreamPrefix, userID) + Starred - itemIDs, err := getItemIDs(r) + itemIDs, err := parseItemIDsFromRequest(r) if err != nil { json.ServerError(w, r, err) return @@ -1058,7 +1034,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque entry.Enclosures.ProxifyEnclosureURL(h.router) contentItems[i] = contentItem{ - ID: fmt.Sprintf(EntryIDLong, entry.ID), + ID: convertEntryIDToLongFormItemID(entry.ID), Title: entry.Title, Author: entry.Author, TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixMicro()), diff --git a/internal/googlereader/item.go b/internal/googlereader/item.go new file mode 100644 index 00000000..fd2d5ad0 --- /dev/null +++ b/internal/googlereader/item.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package googlereader // import "miniflux.app/v2/internal/googlereader" + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "strings" +) + +const ( + ItemIDPrefix = "tag:google.com,2005:reader/item/" +) + +func convertEntryIDToLongFormItemID(entryID int64) string { + // The entry ID is a 64-bit integer, so we need to format it as a 16-character hexadecimal string. + return ItemIDPrefix + fmt.Sprintf("%016x", entryID) +} + +// parseItemID parses a Google Reader ID string. +// It supports both the long form (tag:google.com,2005:reader/item/) and the short form (). +// It returns the parsed ID as a int64 and an error if parsing fails. +func parseItemID(itemIDValue string) (int64, error) { + if strings.HasPrefix(itemIDValue, ItemIDPrefix) { + hexID := strings.TrimPrefix(itemIDValue, ItemIDPrefix) + + // It's always 16 characters wide. + if len(hexID) != 16 { + return 0, errors.New("long form ID has incorrect length") + } + + parsedID, err := strconv.ParseInt(hexID, 16, 64) + if err != nil { + return 0, errors.New("failed to parse long form hex ID: " + err.Error()) + } + return parsedID, nil + } else { + parsedID, err := strconv.ParseInt(itemIDValue, 10, 64) + if err != nil { + return 0, errors.New("failed to parse short form decimal ID: " + err.Error()) + } + return parsedID, nil + } +} + +func parseItemIDsFromRequest(r *http.Request) ([]int64, error) { + items := r.Form[ParamItemIDs] + if len(items) == 0 { + return nil, fmt.Errorf("googlereader: no items requested") + } + + itemIDs := make([]int64, len(items)) + for i, item := range items { + itemID, err := parseItemID(item) + if err != nil { + return nil, fmt.Errorf("googlereader: failed to parse item ID %s: %w", item, err) + } + itemIDs[i] = itemID + } + + return itemIDs, nil +} diff --git a/internal/googlereader/item_test.go b/internal/googlereader/item_test.go new file mode 100644 index 00000000..7b812263 --- /dev/null +++ b/internal/googlereader/item_test.go @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package googlereader // import "miniflux.app/v2/internal/googlereader" + +import ( + "net/http" + "net/url" + "reflect" + "testing" +) + +func TestConvertEntryIDToLongFormItemID(t *testing.T) { + entryID := int64(344691561) + expected := "tag:google.com,2005:reader/item/00000000148b9369" + result := convertEntryIDToLongFormItemID(entryID) + + if result != expected { + t.Errorf("expected %s, got %s", expected, result) + } +} + +func TestParseItemIDsFromRequest(t *testing.T) { + formValues := url.Values{} + formValues.Add("i", "12345") + formValues.Add("i", convertEntryIDToLongFormItemID(45678)) + + request := &http.Request{ + Form: formValues, + } + + result, err := parseItemIDsFromRequest(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var expected = []int64{12345, 45678} + if !reflect.DeepEqual(result, expected) { + t.Errorf("expected %v, got %v", expected, result) + } + + // Test with no item IDs + formValues = url.Values{} + request = &http.Request{ + Form: formValues, + } + _, err = parseItemIDsFromRequest(request) + if err == nil { + t.Fatalf("expected error, got nil") + } +} + +func TestParseItemID(t *testing.T) { + // Test with long form ID + result, err := parseItemID("tag:google.com,2005:reader/item/0000000000000001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := int64(1) + if result != expected { + t.Errorf("expected %d, got %d", expected, result) + } + + // Test with short form ID + result, err = parseItemID("12345") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected = int64(12345) + if result != expected { + t.Errorf("expected %d, got %d", expected, result) + } + + // Test with invalid long form ID + _, err = parseItemID("tag:google.com,2005:reader/item/000000000000000g") + if err == nil { + t.Fatalf("expected error, got nil") + } + + // Test with invalid short form ID + _, err = parseItemID("invalid_id") + if err == nil { + t.Fatalf("expected error, got nil") + } + + // Test with empty ID + _, err = parseItemID("") + if err == nil { + t.Fatalf("expected error, got nil") + } + + // Test with ID that is too short + _, err = parseItemID("tag:google.com,2005:reader/item/00000000000000") + if err == nil { + t.Fatalf("expected error, got nil") + } + + // Test with ID that is too long + _, err = parseItemID("tag:google.com,2005:reader/item/000000000000000000") + if err == nil { + t.Fatalf("expected error, got nil") + } + + // Test with ID that is not a number + _, err = parseItemID("tag:google.com,2005:reader/item/abc") + if err == nil { + t.Fatalf("expected error, got nil") + } +}