OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-sync commits / 6727f4d

readFileRaw: raw-bytes blob read for binary files (0.1.1)
Add execGitBuffer + readFileRaw returning the exact stdout Buffer. readFile coerces git-show output through toString() which corrupts binary blobs (images). pear-git-v3 needs exact bytes to serve committed images over its /raw route. Test proves byte round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6727f4d1c4eb5c3856d90af2bc44f4fbac32ecc8
by plankatron <fleeky@gmail.com> · 2026-06-08T15:17:17.000Z

Changed files (3) +84 -1

lib/browser.js +46 -0
……@@ -42,6 +42,44 @@ function execGit (repoPath, args, timeout) {
4242 })
4343}
4444
45/**
46 * Like execGit but resolves the raw stdout Buffer instead of a UTF-8 string.
47 * Required for binary blobs (images, etc.) — toString() corrupts them.
48 */
49function execGitBuffer (repoPath, args, timeout) {
50 return new Promise((resolve, reject) => {
51 const proc = spawn('git', ['-C', repoPath, ...args], {
52 stdio: ['ignore', 'pipe', 'pipe']
53 })
54
55 const chunks = []
56 const errorChunks = []
57
58 const timer = setTimeout(() => {
59 proc.kill('SIGTERM')
60 reject(new Error('git command timed out: git ' + args.join(' ')))
61 }, timeout || 30000)
62
63 proc.stdout.on('data', (chunk) => chunks.push(chunk))
64 proc.stderr.on('data', (chunk) => errorChunks.push(chunk))
65
66 proc.on('close', (code) => {
67 clearTimeout(timer)
68 if (code !== 0) {
69 const errMsg = Buffer.concat(errorChunks).toString().trim()
70 reject(new Error(errMsg || 'git exited with code ' + code))
71 } else {
72 resolve(Buffer.concat(chunks))
73 }
74 })
75
76 proc.on('error', (err) => {
77 clearTimeout(timer)
78 reject(err)
79 })
80 })
81}
82
4583/**
4684 * Browser methods to mix into HyperSync.prototype
4785 */
……@@ -81,6 +119,14 @@ const browserMethods = {
81119 return execGit(repoPath, ['show', ref + ':' + filePath])
82120 },
83121
122 // Raw blob bytes (Buffer) — for binary files like images that must not be
123 // coerced through toString(). Mirrors readFile but preserves exact bytes.
124 async readFileRaw (name, filePath, ref) {
125 ref = ref || 'HEAD'
126 const repoPath = this._repoPath(name)
127 return execGitBuffer(repoPath, ['show', ref + ':' + filePath])
128 },
129
84130 async getCommits (name, ref, limit) {
85131 ref = ref || 'HEAD'
86132 limit = limit || 20
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-sync",
3 "version": "0.1.0",
3 "version": "0.1.1",
44 "description": "Manage and sync bare git repos over Hyperswarm",
55 "main": "index.js",
66 "imports": {
test/basic.test.js +37 -0
……@@ -168,3 +168,40 @@ test('HyperSync rescan detects changes', async (t) => {
168168 await sync.close();
169169 fs.rmSync(tmpRepos, { recursive: true });
170170});
171
172test('readFileRaw returns exact binary bytes (readFile would corrupt)', async (t) => {
173 // Commit a blob with non-UTF-8 bytes (0x00, 0x80, 0xFF) — a stand-in for an
174 // image. readFile() decodes to a string and mangles these; readFileRaw()
175 // must round-trip them byte-for-byte.
176 const { execSync } = require('child_process');
177 const reposRoot = fs.mkdtempSync(path.join(tmpdir, 'hyper-sync-raw-'));
178 const bareDir = path.join(reposRoot, 'raw-test.git');
179 execSync(`git init --bare ${bareDir}`, { stdio: 'ignore' });
180
181 const work = fs.mkdtempSync(path.join(tmpdir, 'hyper-sync-raw-work-'));
182 const g = (a) => execSync(`git -C ${work} ${a}`, { stdio: 'ignore' });
183 g('init --initial-branch=main');
184 g('config user.email t@t');
185 g('config user.name T');
186
187 const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x80, 0xff, 0x0a, 0x1f, 0x2b]);
188 fs.writeFileSync(path.join(work, 'pixel.png'), bytes);
189 g('add -A');
190 g('commit -m img');
191 g(`push ${bareDir} main`);
192
193 const sync = new HyperSync({ repos: reposRoot });
194 await sync.ready();
195
196 const raw = await sync.readFileRaw('raw-test', 'pixel.png', 'main');
197 t.ok(Buffer.isBuffer(raw), 'readFileRaw returns a Buffer');
198 t.alike(raw, bytes, 'bytes round-trip exactly');
199
200 // Demonstrate why raw is needed: the string path loses the original bytes.
201 const asStr = await sync.readFile('raw-test', 'pixel.png', 'main');
202 t.not(Buffer.from(asStr).length, bytes.length, 'readFile string path corrupts byte length');
203
204 await sync.close();
205 fs.rmSync(reposRoot, { recursive: true });
206 fs.rmSync(work, { recursive: true });
207});