hololake-system-architecture/product-source/hololake-platform/src/utils/vaultAttachments.ts

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
}