diff --git a/.github/workflows/pr.yml b/.github/workflows/test.yml similarity index 75% rename from .github/workflows/pr.yml rename to .github/workflows/test.yml index 9f6dc0d4..cb4cf0ad 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,13 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} timeout-minutes: 60 + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: - uses: actions/checkout@v2 diff --git a/package.json b/package.json index 3266ad66..ba88f58e 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,6 @@ "express": "^4.19.2", "express-async-handler": "^1.2.0", "file-type": "16", - "file-url": "^3.0.0", "fs-extra": "^8.1.0", "i18next": "^22.4.10", "i18next-fs-backend": "^2.1.1", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 88a07cb8..ce88b1b0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -70,7 +70,7 @@ import { isStoreBuild, dragPreventer, havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile, deleteFiles, isOutOfSpaceError, isExecaFailure, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, readDirRecursively, getImportProjectType, - calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, + calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, isWindows, } from './util'; import { toast, errorToast } from './swal'; import { formatDuration } from './util/duration'; @@ -89,11 +89,11 @@ import isDev from './isDev'; import { ChromiumHTMLVideoElement, EdlFileType, FfmpegCommandLog, FormatTimecode, PlaybackMode, SegmentColorIndex, SegmentTags, SegmentToExport, StateSegment, Thumbnail, TunerType } from './types'; import { CaptureFormat, KeyboardAction, Html5ifyMode } from '../../../types'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; +import filePathToUrl from './util/fileUri'; const electron = window.require('electron'); const { exists } = window.require('fs-extra'); const { lstat } = window.require('fs/promises'); -const filePathToUrl = window.require('file-url'); const { parse: parsePath, join: pathJoin, basename, dirname } = window.require('path'); const { focusWindow, hasDisabledNetworking, quitApp } = window.require('@electron/remote').require('./index.js'); @@ -449,7 +449,7 @@ function App() { const effectiveFilePath = previewFilePath || filePath; const fileUri = useMemo(() => { if (!effectiveFilePath) return ''; // Setting video src="" prevents memory leak in chromium - const uri = filePathToUrl(effectiveFilePath); + const uri = filePathToUrl(effectiveFilePath, isWindows); // https://github.com/mifi/lossless-cut/issues/1674 if (cacheBuster !== 0) { const qs = new URLSearchParams(); diff --git a/src/renderer/src/index.tsx b/src/renderer/src/index.tsx index 1f770c67..0c5168ea 100644 --- a/src/renderer/src/index.tsx +++ b/src/renderer/src/index.tsx @@ -42,7 +42,6 @@ declare global { require: (module: T) => ( T extends '@electron/remote' ? TypedRemote : T extends 'electron' ? typeof Electron : - T extends 'file-url' ? typeof import('file-url') : // todo more // eslint-disable-next-line @typescript-eslint/no-explicit-any any diff --git a/src/renderer/src/util/fileUri.test.ts b/src/renderer/src/util/fileUri.test.ts new file mode 100644 index 00000000..5f98a58f --- /dev/null +++ b/src/renderer/src/util/fileUri.test.ts @@ -0,0 +1,48 @@ +import { test, expect, describe } from 'vitest'; + +import fileUriRaw from './fileUri.js'; + + +describe('file uri windows only', () => { + test('converts path to file url', () => { + expect(fileUriRaw('C:\\Users\\sindresorhus\\dev\\te^st.jpg', true)).toEqual('file:///C:/Users/sindresorhus/dev/te%5Est.jpg'); + }); +}); + +describe('file uri non-windows', () => { + // https://github.com/mifi/lossless-cut/issues/1941 + test('file with backslash', () => { + expect(fileUriRaw('/has/back\\slash', false)).toEqual('file:///has/back%5Cslash'); + }); +}); + +// taken from https://github.com/sindresorhus/file-url +describe.each([{ isWindows: false }, { isWindows: true }])('file uri both platforms isWindows=$isWindows', ({ isWindows }) => { + const fileUri = (path) => fileUriRaw(path, isWindows); + + test('converts path to file url', () => { + expect(fileUri('/test.jpg')).toMatch(/file:\/{3}test\.jpg/); + + expect(fileUri('/Users/sindresorhus/dev/te^st.jpg')).toEqual('file:///Users/sindresorhus/dev/te%5Est.jpg'); + }); + + test('escapes more special characters in path', () => { + expect(fileUri('/a?!@#$%^&\'";<>')).toEqual('file:///a%3F!@%23$%25%5E&\'%22;%3C%3E'); + }); + + test('escapes whitespace characters in path', () => { + expect(fileUri('/file with\r\nnewline')).toEqual('file:///file%20with%0D%0Anewline'); + }); + + test('relative path', () => { + expect(fileUri('relative/test.jpg')).toEqual('file:///relative/test.jpg'); + }); + + test('empty', () => { + expect(fileUri('')).toEqual('file:///'); + }); + + test('slash', () => { + expect(fileUri('/')).toEqual('file:///'); + }); +}); diff --git a/src/renderer/src/util/fileUri.ts b/src/renderer/src/util/fileUri.ts new file mode 100644 index 00000000..df955ee7 --- /dev/null +++ b/src/renderer/src/util/fileUri.ts @@ -0,0 +1,17 @@ +export default function fileUri(filePath: string, isWindows: boolean) { + let pathName = filePath; + + if (isWindows) { + pathName = pathName.replaceAll('\\', '/'); + } + + // Windows drive letter must be prefixed with a slash. + // also relative paths will be converted to absolute + if (pathName[0] !== '/') { + pathName = `/${pathName}`; + } + + // Escape required characters for path components. + // See: https://tools.ietf.org/html/rfc3986#section-3.3 + return encodeURI(`file://${pathName}`).replaceAll(/[#?]/g, encodeURIComponent); +} diff --git a/yarn.lock b/yarn.lock index ba893d4a..f97d60d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5804,13 +5804,6 @@ __metadata: languageName: node linkType: hard -"file-url@npm:^3.0.0": - version: 3.0.0 - resolution: "file-url@npm:3.0.0" - checksum: f15c1bdd81df1a09238f3411f877274d7849703df837ec327c4d1df631314f60036cb700a59d826d8c96b79ff66429d3c758480005e1899c00961541b98d5bfe - languageName: node - linkType: hard - "filelist@npm:^1.0.1": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -7956,7 +7949,6 @@ __metadata: express-async-handler: "npm:^1.2.0" fast-xml-parser: "npm:^4.2.5" file-type: "npm:16" - file-url: "npm:^3.0.0" framer-motion: "npm:^9.0.3" fs-extra: "npm:^8.1.0" i18next: "npm:^22.4.10"