250 lines
7.8 KiB
TypeScript
250 lines
7.8 KiB
TypeScript
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
import { canWritePathToVault } from './vaultPathContainment'
|
|
|
|
const LOCALHOST_ASSET_URL_PREFIX = 'asset://localhost/'
|
|
const HTTP_ASSET_URL_PREFIX = 'http://asset.localhost/'
|
|
const GENERIC_ASSET_URL_PREFIX = 'asset://'
|
|
const ASSET_URL_PREFIXES = [
|
|
LOCALHOST_ASSET_URL_PREFIX,
|
|
HTTP_ASSET_URL_PREFIX,
|
|
GENERIC_ASSET_URL_PREFIX,
|
|
]
|
|
const ATTACHMENTS_SEGMENT = '/attachments/'
|
|
const RELATIVE_ATTACHMENTS_PREFIX = 'attachments/'
|
|
const WINDOWS_EXTENDED_PATH_PREFIX = '\\\\?\\'
|
|
const WINDOWS_EXTENDED_UNC_PREFIX = '\\\\?\\UNC\\'
|
|
const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/
|
|
|
|
type AbsolutePath = string
|
|
type AttachmentPath = string
|
|
type VaultPath = string
|
|
|
|
type UrlRequest = {
|
|
url: string
|
|
}
|
|
|
|
type PathRequest = {
|
|
path: string
|
|
}
|
|
|
|
type VaultPathRequest = {
|
|
vaultPath: VaultPath
|
|
}
|
|
|
|
type AttachmentPathRequest = {
|
|
attachmentPath: AttachmentPath
|
|
}
|
|
|
|
export type VaultAttachmentPathRequest = UrlRequest & {
|
|
vaultPath?: VaultPath
|
|
}
|
|
|
|
export type VaultAttachmentUrlRequest = UrlRequest
|
|
|
|
function safeDecode(value: string): string {
|
|
try {
|
|
return decodeURIComponent(value)
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
|
|
function hasUnsafeRelativeSegment({ path }: PathRequest): boolean {
|
|
return path.split(/[\\/]/u).some(segment => segment === '..')
|
|
}
|
|
|
|
function usesWindowsSeparators({ path }: PathRequest): boolean {
|
|
return WINDOWS_DRIVE_PATH_PATTERN.test(path) || path.startsWith('\\\\')
|
|
}
|
|
|
|
function relativePathForVault({
|
|
attachmentPath,
|
|
vaultPath,
|
|
}: AttachmentPathRequest & VaultPathRequest): AttachmentPath {
|
|
return usesWindowsSeparators({ path: vaultPath })
|
|
? attachmentPath.replace(/\//g, '\\')
|
|
: attachmentPath.replace(/\\/g, '/')
|
|
}
|
|
|
|
function removeWindowsExtendedPrefix(path: AbsolutePath): AbsolutePath {
|
|
if (path.startsWith(WINDOWS_EXTENDED_UNC_PREFIX)) {
|
|
return `\\\\${path.slice(WINDOWS_EXTENDED_UNC_PREFIX.length)}`
|
|
}
|
|
if (path.startsWith(WINDOWS_EXTENDED_PATH_PREFIX)) {
|
|
return path.slice(WINDOWS_EXTENDED_PATH_PREFIX.length)
|
|
}
|
|
return path
|
|
}
|
|
|
|
function normalizedFilesystemPath(path: AbsolutePath): AbsolutePath {
|
|
return removeWindowsExtendedPrefix(path).replace(/\\/g, '/')
|
|
}
|
|
|
|
function withoutTrailingSlash(path: AbsolutePath): AbsolutePath {
|
|
return path.replace(/\/+$/, '')
|
|
}
|
|
|
|
function normalizeForComparison(path: AbsolutePath, vaultPath: VaultPath): AbsolutePath {
|
|
const normalizedPath = withoutTrailingSlash(normalizedFilesystemPath(path))
|
|
const normalizedVaultPath = withoutTrailingSlash(normalizedFilesystemPath(vaultPath))
|
|
return WINDOWS_DRIVE_PATH_PATTERN.test(normalizedPath)
|
|
|| WINDOWS_DRIVE_PATH_PATTERN.test(normalizedVaultPath)
|
|
? normalizedPath.toLowerCase()
|
|
: normalizedPath
|
|
}
|
|
|
|
function assetUrlPrefix({ url }: UrlRequest): string | null {
|
|
return ASSET_URL_PREFIXES.find(prefix => url.startsWith(prefix)) ?? null
|
|
}
|
|
|
|
function isRootedPath({ path }: PathRequest): boolean {
|
|
if (path.startsWith('/')) return true
|
|
if (WINDOWS_DRIVE_PATH_PATTERN.test(path)) return true
|
|
return path.startsWith('\\\\')
|
|
}
|
|
|
|
function restoreUnixRoot({ path }: PathRequest): AbsolutePath {
|
|
return isRootedPath({ path }) ? path : `/${path}`
|
|
}
|
|
|
|
function resolveAssetPath({ url }: UrlRequest): AbsolutePath | null {
|
|
const prefix = assetUrlPrefix({ url })
|
|
if (!prefix) return null
|
|
return restoreUnixRoot({ path: safeDecode(url.slice(prefix.length)) })
|
|
}
|
|
|
|
function extractPortableAttachmentPath({ path }: PathRequest): AttachmentPath | null {
|
|
const normalizedPath = normalizedFilesystemPath(path)
|
|
const index = normalizedPath.lastIndexOf(ATTACHMENTS_SEGMENT)
|
|
if (index === -1) return null
|
|
|
|
const filename = normalizedPath.slice(index + ATTACHMENTS_SEGMENT.length)
|
|
return filename ? `${RELATIVE_ATTACHMENTS_PREFIX}${filename}` : null
|
|
}
|
|
|
|
function currentVaultAttachmentPath({
|
|
path,
|
|
vaultPath,
|
|
}: PathRequest & VaultPathRequest): AttachmentPath | null {
|
|
const normalizedPath = normalizedFilesystemPath(path)
|
|
const normalizedVaultPath = withoutTrailingSlash(normalizedFilesystemPath(vaultPath))
|
|
const attachmentsRoot = `${normalizedVaultPath}/${RELATIVE_ATTACHMENTS_PREFIX}`
|
|
const comparablePath = normalizeForComparison(normalizedPath, vaultPath)
|
|
const comparableRoot = normalizeForComparison(attachmentsRoot, vaultPath)
|
|
if (!comparablePath.startsWith(comparableRoot)) return null
|
|
|
|
const filename = normalizedPath.slice(attachmentsRoot.length)
|
|
return filename ? `${RELATIVE_ATTACHMENTS_PREFIX}${filename}` : null
|
|
}
|
|
|
|
function isPathInsideVault({
|
|
path,
|
|
vaultPath,
|
|
}: PathRequest & VaultPathRequest): boolean {
|
|
return canWritePathToVault(
|
|
removeWindowsExtendedPrefix(path),
|
|
removeWindowsExtendedPrefix(vaultPath),
|
|
)
|
|
}
|
|
|
|
function resolveRelativeAttachmentPath({
|
|
url,
|
|
vaultPath,
|
|
}: UrlRequest & VaultPathRequest): AbsolutePath | null {
|
|
if (!url.startsWith(RELATIVE_ATTACHMENTS_PREFIX)) return null
|
|
|
|
const attachmentPath = safeDecode(url)
|
|
if (hasUnsafeRelativeSegment({ path: attachmentPath })) return null
|
|
return vaultAttachmentPath({ vaultPath, attachmentPath })
|
|
}
|
|
|
|
export function vaultAttachmentPath({
|
|
attachmentPath,
|
|
vaultPath,
|
|
}: AttachmentPathRequest & VaultPathRequest): AbsolutePath {
|
|
const separator = usesWindowsSeparators({ path: vaultPath }) ? '\\' : '/'
|
|
const normalizedAttachmentPath = relativePathForVault({ vaultPath, attachmentPath })
|
|
const joiner = vaultPath.endsWith('/') || vaultPath.endsWith('\\') ? '' : separator
|
|
return `${vaultPath}${joiner}${normalizedAttachmentPath}`
|
|
}
|
|
|
|
export function attachmentAssetUrlFromPath({ path }: PathRequest): string {
|
|
return convertFileSrc(path)
|
|
}
|
|
|
|
export function vaultAttachmentAssetUrl({
|
|
attachmentPath,
|
|
vaultPath,
|
|
}: AttachmentPathRequest & VaultPathRequest): string {
|
|
return attachmentAssetUrlFromPath({
|
|
path: vaultAttachmentPath({ vaultPath, attachmentPath }),
|
|
})
|
|
}
|
|
|
|
export function isPortableAttachmentPath({ path }: PathRequest): boolean {
|
|
return path.startsWith(RELATIVE_ATTACHMENTS_PREFIX)
|
|
}
|
|
|
|
export function isTauriAssetUrl({ url }: UrlRequest): boolean {
|
|
return assetUrlPrefix({ url }) !== null
|
|
}
|
|
|
|
export function filesystemPathFromAssetUrl({ url }: UrlRequest): AbsolutePath | null {
|
|
return resolveAssetPath({ url })
|
|
}
|
|
|
|
export function isCurrentVaultAssetUrl({
|
|
url,
|
|
vaultPath,
|
|
}: UrlRequest & VaultPathRequest): boolean {
|
|
const path = resolveAssetPath({ url })
|
|
return path ? isPathInsideVault({ path, vaultPath }) : false
|
|
}
|
|
|
|
export function portableAttachmentPathFromCurrentVaultAssetUrl({
|
|
url,
|
|
vaultPath,
|
|
}: UrlRequest & VaultPathRequest): AttachmentPath | null {
|
|
const path = resolveAssetPath({ url })
|
|
if (!path || !isPathInsideVault({ path, vaultPath })) return null
|
|
return currentVaultAttachmentPath({ path, vaultPath })
|
|
}
|
|
|
|
export function portableAttachmentPathFromCurrentVaultPath({
|
|
path,
|
|
vaultPath,
|
|
}: PathRequest & VaultPathRequest): AttachmentPath | null {
|
|
if (!isPathInsideVault({ path, vaultPath })) return null
|
|
return currentVaultAttachmentPath({ path, vaultPath })
|
|
}
|
|
|
|
export function portableAttachmentPathFromAnyAssetUrl({ url }: UrlRequest): AttachmentPath | null {
|
|
const path = resolveAssetPath({ url })
|
|
return path ? extractPortableAttachmentPath({ path }) : null
|
|
}
|
|
|
|
export function isVaultAttachmentUrl({ url }: VaultAttachmentUrlRequest): boolean {
|
|
const trimmedUrl = url.trim()
|
|
return isPortableAttachmentPath({ path: trimmedUrl })
|
|
|| isTauriAssetUrl({ url: trimmedUrl })
|
|
}
|
|
|
|
export function resolveVaultAttachmentPath({
|
|
url,
|
|
vaultPath,
|
|
}: VaultAttachmentPathRequest): AbsolutePath | null {
|
|
const trimmedVaultPath = vaultPath?.trim()
|
|
if (!trimmedVaultPath) return null
|
|
|
|
const trimmedUrl = url.trim()
|
|
if (!trimmedUrl) return null
|
|
|
|
const candidatePath = resolveRelativeAttachmentPath({
|
|
url: trimmedUrl,
|
|
vaultPath: trimmedVaultPath,
|
|
})
|
|
?? resolveAssetPath({ url: trimmedUrl })
|
|
?? trimmedUrl
|
|
|
|
return isPathInsideVault({ path: candidatePath, vaultPath: trimmedVaultPath }) ? candidatePath : null
|
|
}
|