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  }