github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/pkg/service/series.go (about) 1 // Copyright 2025 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package service 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 12 "github.com/google/syzkaller/syz-cluster/pkg/api" 13 "github.com/google/syzkaller/syz-cluster/pkg/app" 14 "github.com/google/syzkaller/syz-cluster/pkg/blob" 15 "github.com/google/syzkaller/syz-cluster/pkg/db" 16 "github.com/google/uuid" 17 ) 18 19 // SeriesService is tested in controller/. 20 21 type SeriesService struct { 22 sessionRepo *db.SessionRepository 23 seriesRepo *db.SeriesRepository 24 blobStorage blob.Storage 25 } 26 27 func NewSeriesService(env *app.AppEnvironment) *SeriesService { 28 return &SeriesService{ 29 sessionRepo: db.NewSessionRepository(env.Spanner), 30 seriesRepo: db.NewSeriesRepository(env.Spanner), 31 blobStorage: env.BlobStorage, 32 } 33 } 34 35 func (s *SeriesService) GetSessionSeries(ctx context.Context, sessionID string) (*api.Series, error) { 36 return s.getSessionSeries(ctx, sessionID, true) 37 } 38 39 func (s *SeriesService) GetSessionSeriesShort(ctx context.Context, 40 sessionID string) (*api.Series, error) { 41 return s.getSessionSeries(ctx, sessionID, false) 42 } 43 44 func (s *SeriesService) getSessionSeries(ctx context.Context, sessionID string, 45 includePatches bool) (*api.Series, error) { 46 session, err := s.sessionRepo.GetByID(ctx, sessionID) 47 if err != nil { 48 return nil, fmt.Errorf("failed to fetch the session: %w", err) 49 } else if session == nil { 50 return nil, fmt.Errorf("%w: %q", ErrSessionNotFound, sessionID) 51 } 52 return s.getSeries(ctx, session.SeriesID, includePatches) 53 } 54 55 func (s *SeriesService) UploadSeries(ctx context.Context, series *api.Series) (*api.UploadSeriesResp, error) { 56 seriesObj := &db.Series{ 57 ID: uuid.NewString(), 58 ExtID: series.ExtID, 59 AuthorEmail: series.AuthorEmail, 60 Title: series.Title, 61 Version: int64(series.Version), 62 Link: series.Link, 63 PublishedAt: series.PublishedAt, 64 Cc: series.Cc, 65 } 66 for _, tag := range series.SubjectTags { 67 const tageSizeLimit = 511 68 if len(tag) > tageSizeLimit { 69 tag = tag[:tageSizeLimit] 70 } 71 seriesObj.SubjectTags = append(seriesObj.SubjectTags, tag) 72 } 73 err := s.seriesRepo.Insert(ctx, seriesObj, func() ([]*db.Patch, error) { 74 var ret []*db.Patch 75 for _, patch := range series.Patches { 76 // In case of errors, we will waste some space, but let's ignore it for simplicity. 77 // Patches are not super big. 78 uri, err := s.blobStorage.Write(bytes.NewReader(patch.Body), 79 "Series", seriesObj.ID, "Patches", fmt.Sprint(patch.Seq)) 80 if err != nil { 81 return nil, fmt.Errorf("failed to upload patch body: %w", err) 82 } 83 ret = append(ret, &db.Patch{ 84 Seq: int64(patch.Seq), 85 Title: patch.Title, 86 Link: patch.Link, 87 BodyURI: uri, 88 }) 89 } 90 return ret, nil 91 }) 92 if err != nil { 93 if errors.Is(err, db.ErrSeriesExists) { 94 return &api.UploadSeriesResp{Saved: false}, nil 95 } 96 return nil, err 97 } 98 return &api.UploadSeriesResp{ 99 ID: seriesObj.ID, 100 Saved: true, 101 }, nil 102 } 103 104 var ErrSeriesNotFound = errors.New("series not found") 105 106 func (s *SeriesService) GetSeries(ctx context.Context, seriesID string) (*api.Series, error) { 107 return s.getSeries(ctx, seriesID, true) 108 } 109 110 func (s *SeriesService) getSeries(ctx context.Context, 111 seriesID string, includeBody bool) (*api.Series, error) { 112 series, err := s.seriesRepo.GetByID(ctx, seriesID) 113 if err != nil { 114 return nil, fmt.Errorf("failed to fetch the series: %w", err) 115 } else if series == nil { 116 return nil, ErrSeriesNotFound 117 } 118 patches, err := s.seriesRepo.ListPatches(ctx, series) 119 if err != nil { 120 return nil, fmt.Errorf("failed to fetch patches: %w", err) 121 } 122 ret := &api.Series{ 123 ID: series.ID, 124 ExtID: series.ExtID, 125 Title: series.Title, 126 AuthorEmail: series.AuthorEmail, 127 Version: int(series.Version), 128 Cc: series.Cc, 129 PublishedAt: series.PublishedAt, 130 Link: series.Link, 131 SubjectTags: series.SubjectTags, 132 } 133 for _, patch := range patches { 134 var body []byte 135 if includeBody { 136 body, err = blob.ReadAllBytes(s.blobStorage, patch.BodyURI) 137 if err != nil { 138 return nil, fmt.Errorf("failed to read patch %q: %w", patch.ID, err) 139 } 140 } 141 ret.Patches = append(ret.Patches, api.SeriesPatch{ 142 Seq: int(patch.Seq), 143 Title: patch.Title, 144 Link: patch.Link, 145 Body: body, 146 }) 147 } 148 return ret, nil 149 }