返回 DeepSeek-Reasonix
content.go
根目录 / internal / extension / sidecar / content.go
1 package sidecar
2
3 import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/base64"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "sync"
13
14 "reasonix/internal/extension/protocol"
15 )
16
17 // storeMaxEntries bounds the per-client content store; the oldest object is
18 // evicted past the cap so a chatty sidecar cannot grow host memory without
19 // limit. Mirrored from the Remote workbench content store.
20 const storeMaxEntries = 64
21
22 // ExternalizedField is the host-side mirror of the schema's externalized
23 // field metadata: when an externalizable payload exceeds
24 // protocol.ExternalizeFieldBytes it travels as a content ref, and this
25 // descriptor tells the extension where to page the real bytes back from.
26 type ExternalizedField = protocol.ExternalizedField
27
28 type contentObject struct {
29 data []byte
30 sha256 string
31 order uint64
32 }
33
34 // Store holds externalized content for exactly one sidecar connection. It is
35 // session-scoped in memory only: refs expire with the connection, which is
36 // why a read of an unknown ref answers content_ref_expired.
37 type Store struct {
38 mu sync.Mutex
39 contents map[string]contentObject
40 nextOrder uint64
41 }
42
43 // NewStore returns an empty content store.
44 func NewStore() *Store {
45 return &Store{contents: make(map[string]contentObject)}
46 }
47
48 // Put stores data and returns its ref, SHA-256 hex digest, and byte count.
49 // Objects larger than protocol.ContentRefObjectBytes are rejected with the
50 // frozen frame_too_large error; past storeMaxEntries the oldest object is
51 // evicted.
52 func (s *Store) Put(data []byte) (ref string, digest string, totalBytes int64, err error) {
53 if len(data) > protocol.ContentRefObjectBytes {
54 return "", "", 0, protocol.MustProtocolError(protocol.ErrFrameTooLarge)
55 }
56 sum := sha256.Sum256(data)
57 digest = hex.EncodeToString(sum[:])
58 ref = "content_" + randomHex(12)
59
60 s.mu.Lock()
61 defer s.mu.Unlock()
62 s.nextOrder++
63 s.contents[ref] = contentObject{data: append([]byte(nil), data...), sha256: digest, order: s.nextOrder}
64 if len(s.contents) > storeMaxEntries {
65 var oldestRef string
66 var oldestOrder uint64
67 for candidate, object := range s.contents {
68 if candidate == ref {
69 continue
70 }
71 if oldestRef == "" || object.order < oldestOrder {
72 oldestRef, oldestOrder = candidate, object.order
73 }
74 }
75 delete(s.contents, oldestRef)
76 }
77 return ref, digest, int64(len(data)), nil
78 }
79
80 // Read pages one chunk of at most protocol.ContentRefChunkBytes from ref at
81 // offset, returning the chunk, the next offset (nil at end of object), the
82 // total byte count, and the object's SHA-256. Unknown refs and out-of-range
83 // offsets answer the frozen content_ref_expired error. This is the in-process
84 // form of ReadHandler: the host uses it to resolve content refs a peer hands
85 // back (e.g. an externalized intercept replacement) with the exact
86 // host/content/read chunking rules.
87 func (s *Store) Read(ref string, offset int64) (chunk []byte, next *int64, totalBytes int64, digest string, err error) {
88 s.mu.Lock()
89 object, ok := s.contents[ref]
90 s.mu.Unlock()
91 if !ok {
92 return nil, nil, 0, "", protocol.MustProtocolError(protocol.ErrContentRefExpired)
93 }
94 if offset < 0 || offset > int64(len(object.data)) {
95 return nil, nil, 0, "", protocol.MustProtocolError(protocol.ErrContentRefExpired)
96 }
97 end := offset + protocol.ContentRefChunkBytes
98 if end > int64(len(object.data)) {
99 end = int64(len(object.data))
100 }
101 if end < int64(len(object.data)) {
102 value := end
103 next = &value
104 }
105 return object.data[offset:end], next, int64(len(object.data)), object.sha256, nil
106 }
107
108 // ReadHandler answers host/content/read: one page of at most
109 // protocol.ContentRefChunkBytes starting at the exact requested offset. The
110 // final chunk omits NextOffset. Unknown refs and out-of-range offsets answer
111 // the frozen content_ref_expired error, mirroring the Remote workbench.
112 func (s *Store) ReadHandler(_ context.Context, raw json.RawMessage) (any, error) {
113 decoded, err := protocol.DecodeExtensionRequestParams(protocol.MethodHostContentRead, raw)
114 if err != nil {
115 return nil, protocol.MustProtocolError(protocol.ErrInvalidParams).RPCError()
116 }
117 p := decoded.(protocol.ContentReadParams)
118 chunk, next, totalBytes, digest, err := s.Read(p.ContentRef, p.Offset)
119 if err != nil {
120 var protocolErr *protocol.ProtocolError
121 if errors.As(err, &protocolErr) {
122 return nil, protocolErr.RPCError()
123 }
124 return nil, err
125 }
126 return protocol.ContentReadResult{
127 ContentRef: p.ContentRef, Offset: p.Offset,
128 DataBase64: base64.StdEncoding.EncodeToString(chunk), NextOffset: next,
129 TotalBytes: totalBytes, SHA256: digest, Encoding: protocol.ContentUTF8,
130 }, nil
131 }
132
133 // MaybeExternalize stores value and returns its descriptor when it exceeds
134 // the frozen externalization threshold; smaller values pass through as a nil
135 // descriptor, meaning the caller keeps them inline. field is the RFC 6901
136 // JSON pointer of the externalizable field inside its owner document.
137 func MaybeExternalize(store *Store, field string, value []byte) (*ExternalizedField, error) {
138 if !protocol.RequiresExternalization(len(value)) {
139 return nil, nil
140 }
141 if store == nil {
142 return nil, fmt.Errorf("sidecar: externalizing %s requires a content store", field)
143 }
144 ref, digest, totalBytes, err := store.Put(value)
145 if err != nil {
146 return nil, err
147 }
148 return &ExternalizedField{
149 JSONPointer: field, ContentRef: ref, TotalBytes: totalBytes, SHA256: digest,
150 }, nil
151 }
152
153 // randomHex returns n random bytes hex-encoded (2n chars) for content refs.
154 func randomHex(n int) string {
155 buf := make([]byte, n)
156 if _, err := rand.Read(buf); err != nil {
157 panic(fmt.Sprintf("sidecar: crypto/rand unavailable: %v", err))
158 }
159 return hex.EncodeToString(buf)
160 }
161
161 lines GO