package research import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "strings" "git.cin.su/m/Assistant/contracts/epistemics" ) var ErrRepositoryPathRejected = errors.New("repository source path rejected") type RepositoryConnector struct{ Root string } func (c RepositoryConnector) Collect(ctx context.Context, request epistemics.RepositoryResearchRequest, nowUnixMillis int64) (epistemics.ResearchCandidate, error) { if err := request.Plan.Validate(); err != nil { return epistemics.ResearchCandidate{}, err } if request.SourceID == "" || strings.TrimSpace(request.Statement) == "" || nowUnixMillis < 1 { return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected } root, err := filepath.Abs(c.Root) if err != nil || root == "" { return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected } path, err := filepath.Abs(filepath.Join(root, filepath.FromSlash(request.Path))) if err != nil || !withinRoot(root, path) { return epistemics.ResearchCandidate{}, ErrRepositoryPathRejected } select { case <-ctx.Done(): return epistemics.ResearchCandidate{}, ctx.Err() default: } content, err := os.ReadFile(path) if err != nil || len(content) == 0 { return epistemics.ResearchCandidate{}, fmt.Errorf("read repository source: %w", err) } digest := sha256.Sum256(content) digestText := "sha256:" + hex.EncodeToString(digest[:]) key := sha256.Sum256([]byte(filepath.ToSlash(path) + "\x00" + digestText)) keyText := hex.EncodeToString(key[:]) return epistemics.ResearchCandidate{ PropositionKey: request.PropositionKey, Statement: request.Statement, Source: epistemics.Source{ID: request.SourceID, Kind: "repository", Locator: "file://" + filepath.ToSlash(path), Title: filepath.Base(path), Publisher: "local-repository"}, Snapshot: epistemics.SourceSnapshot{ID: epistemics.SourceSnapshotID("snapshot:" + keyText), SourceID: request.SourceID, ContentDigest: digestText, CapturedAtUnixMillis: nowUnixMillis, Freshness: epistemics.FreshnessCurrent}, Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID("excerpt:" + keyText), SnapshotID: epistemics.SourceSnapshotID("snapshot:" + keyText), Text: string(content), Start: 0, End: int64(len(content))}, Evidence: epistemics.EvidenceObserved, Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone, }, nil } func withinRoot(root, path string) bool { rel, err := filepath.Rel(root, path) return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && !filepath.IsAbs(rel) }