diff --git a/packages/fuse-daemon/internal/filesystem/file.go b/packages/fuse-daemon/internal/filesystem/file.go index 6c39c4e4d..943aec36a 100644 --- a/packages/fuse-daemon/internal/filesystem/file.go +++ b/packages/fuse-daemon/internal/filesystem/file.go @@ -2,6 +2,7 @@ package filesystem import ( "context" + "encoding/base64" "strconv" "log/slog" @@ -64,7 +65,7 @@ func (f *InternxtFile) Read(dest []byte, off int64) (fuse.ReadResult, fuse.Statu func (f *InternxtFile) Write(data []byte, off int64) (uint32, fuse.Status) { f.logger.Debug("Received Write call", "path", f.path, "offset", off, "length", len(data)) headers := map[string]string{ - "X-Path": f.path, + "X-Path-B64": base64.StdEncoding.EncodeToString([]byte(f.path)), "X-Offset": strconv.FormatInt(off, 10), } diff --git a/src/backend/features/virtual-drive/controllers/operations/write.controller.test.ts b/src/backend/features/virtual-drive/controllers/operations/write.controller.test.ts index 660fc9157..8134e9734 100644 --- a/src/backend/features/virtual-drive/controllers/operations/write.controller.test.ts +++ b/src/backend/features/virtual-drive/controllers/operations/write.controller.test.ts @@ -21,7 +21,7 @@ describe('writeController', () => { }); it('should return errno EINVAL when payload is invalid', async () => { - req.header.calledWith('X-Path').mockReturnValue('/some/file.txt'); + req.header.calledWith('X-Path-B64').mockReturnValue(Buffer.from('/some/file.txt', 'utf8').toString('base64')); req.header.calledWith('X-Offset').mockReturnValue('wrong'); req.body = Buffer.from('hello'); @@ -33,7 +33,7 @@ describe('writeController', () => { }); it('should return errno 0 and written bytes when write succeeds', async () => { - req.header.calledWith('X-Path').mockReturnValue('/some/file.txt'); + req.header.calledWith('X-Path-B64').mockReturnValue(Buffer.from('/some/file.txt', 'utf8').toString('base64')); req.header.calledWith('X-Offset').mockReturnValue('0'); req.body = Buffer.from('hello'); writeMock.mockResolvedValue({ data: 5 }); @@ -46,7 +46,7 @@ describe('writeController', () => { }); it('should return errno EIO when write fails', async () => { - req.header.calledWith('X-Path').mockReturnValue('/some/file.txt'); + req.header.calledWith('X-Path-B64').mockReturnValue(Buffer.from('/some/file.txt', 'utf8').toString('base64')); req.header.calledWith('X-Offset').mockReturnValue('0'); req.body = Buffer.from('hello'); writeMock.mockResolvedValue({ error: new FuseError(FuseCodes.EIO, 'io error') }); @@ -56,4 +56,39 @@ describe('writeController', () => { expect(res.set).toHaveBeenCalledWith('X-Errno', String(FuseCodes.EIO)); expect(res.send).toHaveBeenCalledWith(Buffer.alloc(0)); }); + + it('should decode UTF-8 path from base64 header before write', async () => { + const encodedPath = Buffer.from('/тестовое изображение.jpeg', 'utf8').toString('base64'); + req.header.calledWith('X-Path-B64').mockReturnValue(encodedPath); + req.header.calledWith('X-Offset').mockReturnValue('0'); + req.body = Buffer.from('hello'); + writeMock.mockResolvedValue({ data: 5 }); + + await writeController(req, res, container); + + expect(writeMock).toHaveBeenCalledWith({ + path: '/тестовое изображение.jpeg', + content: Buffer.from('hello'), + offset: 0, + container, + }); + }); + + it('should decode base64 path from header when filename contains newline', async () => { + const newlinePath = '/nombre\narchivo.txt'; + const encodedPath = Buffer.from(newlinePath, 'utf8').toString('base64'); + req.header.calledWith('X-Path-B64').mockReturnValue(encodedPath); + req.header.calledWith('X-Offset').mockReturnValue('0'); + req.body = Buffer.from('hello'); + writeMock.mockResolvedValue({ data: 5 }); + + await writeController(req, res, container); + + expect(writeMock).toHaveBeenCalledWith({ + path: newlinePath, + content: Buffer.from('hello'), + offset: 0, + container, + }); + }); }); diff --git a/src/backend/features/virtual-drive/controllers/operations/write.controller.ts b/src/backend/features/virtual-drive/controllers/operations/write.controller.ts index e29687d64..0c4591420 100644 --- a/src/backend/features/virtual-drive/controllers/operations/write.controller.ts +++ b/src/backend/features/virtual-drive/controllers/operations/write.controller.ts @@ -6,13 +6,13 @@ import { write } from '../../services/operations/write.service'; import { ensureLeadingSlash } from '../ensure-leading-slash'; export async function writeController(req: Request, res: Response, container: Container) { - const rawPath = req.header('X-Path'); + const rawBase64Path = req.header('X-Path-B64'); const rawOffset = req.header('X-Offset'); const body = req.body; const content = Buffer.isBuffer(body) ? body : undefined; const offset = rawOffset ? Number.parseInt(rawOffset, 10) : Number.NaN; - if (typeof rawPath !== 'string' || Number.isNaN(offset) || !content) { + if (typeof rawBase64Path !== 'string' || Number.isNaN(offset) || !content) { logger.error({ msg: '[FUSE DAEMON] Write: missing required fields', headers: req.headers, @@ -22,8 +22,7 @@ export async function writeController(req: Request, res: Response, container: Co res.send(Buffer.alloc(0)); return; } - - const normalizedPath = ensureLeadingSlash(rawPath); + const normalizedPath = ensureLeadingSlash(Buffer.from(rawBase64Path, 'base64').toString('utf8')); logger.debug({ msg: '[FUSE DAEMON] Write signal received',