file-address.spec.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { describe, expect, it } from 'vitest'
  2. import { absoluteFileAddress, parseFileAddress, sessionFileAddress } from '../src/file-address.ts'
  3. describe('file addresses', () => {
  4. it('round-trips a session-relative path with encoded segments', () => {
  5. const address = sessionFileAddress('s 1', 'w/a b#c?.txt')
  6. expect(address).toBe('dsh-resource://file/session/s%201/w/a%20b%23c%3F.txt')
  7. expect(parseFileAddress(address)).toEqual({ scope: 'session', sessionId: 's 1', path: 'w/a b#c?.txt' })
  8. })
  9. it('drops a leading ./ from a session-relative path and names the root with an empty path', () => {
  10. expect(sessionFileAddress('s', './src/a.ts')).toBe('dsh-resource://file/session/s/src/a.ts')
  11. expect(sessionFileAddress('s', '././../outside/a.txt')).toBe('dsh-resource://file/session/s/../outside/a.txt')
  12. expect(sessionFileAddress('s', 'src\\a.ts')).toBe('dsh-resource://file/session/s/src/a.ts')
  13. expect(sessionFileAddress('s', '')).toBe('dsh-resource://file/session/s/')
  14. expect(parseFileAddress('dsh-resource://file/session/s/')).toEqual({ scope: 'session', sessionId: 's', path: '' })
  15. })
  16. it.each([
  17. ['/etc/hosts', '/etc/hosts', 'dsh-resource://file/session/s//etc/hosts'],
  18. ['/', '/', 'dsh-resource://file/session/s//'],
  19. ['C:\\w\\x.ts', 'C:/w/x.ts', 'dsh-resource://file/session/s/C:/w/x.ts'],
  20. ['\\\\server\\share\\x.ts', '//server/share/x.ts', 'dsh-resource://file/session/s///server/share/x.ts'],
  21. ['/a b#c?%.txt', '/a b#c?%.txt', 'dsh-resource://file/session/s//a%20b%23c%3F%25.txt'],
  22. ])('round-trips the absolute path %s in a Session address', (input, path, expected) => {
  23. const address = sessionFileAddress('s', input)
  24. expect(address).toBe(expected)
  25. expect(parseFileAddress(address)).toEqual({ scope: 'session', sessionId: 's', path })
  26. })
  27. it('gives the same absolute path different addresses in different Sessions', () => {
  28. expect(sessionFileAddress('s1', '/etc/hosts')).toBe('dsh-resource://file/session/s1//etc/hosts')
  29. expect(sessionFileAddress('s2', '/etc/hosts')).toBe('dsh-resource://file/session/s2//etc/hosts')
  30. })
  31. it.each([
  32. '../outside/a.txt',
  33. 'dir/../outside/a.txt',
  34. 'dir/./a.txt',
  35. '/workspace/../outside/a.txt',
  36. 'C:/workspace/../outside/a.txt',
  37. '//server/share/workspace/../outside/a.txt',
  38. ])('keeps the Session id and the unresolved path %s', (path) => {
  39. expect(parseFileAddress(sessionFileAddress('s 1', path))).toEqual({ scope: 'session', sessionId: 's 1', path })
  40. })
  41. it.each([
  42. '/workspace/../outside/a.txt',
  43. 'C:/workspace/../outside/a.txt',
  44. '//server/share/workspace/../outside/a.txt',
  45. ])('keeps the unresolved absolute path %s without a Session', (path) => {
  46. expect(parseFileAddress(absoluteFileAddress(path))).toEqual({ scope: 'absolute', path })
  47. })
  48. it.each([
  49. ['./a.txt', './a.txt'],
  50. ['%2e%2E/outside/a.txt', '../outside/a.txt'],
  51. ['dir/%2E/a.txt', 'dir/./a.txt'],
  52. ['%252e%252e/a.txt', '%2e%2e/a.txt'],
  53. ['a%2Fb%5Cc.txt', 'a/b\\c.txt'],
  54. ])('decodes %s once without resolving dot segments', (encoded, path) => {
  55. expect(parseFileAddress(`dsh-resource://file/session/s%2F1/${encoded}`)).toEqual({ scope: 'session', sessionId: 's/1', path })
  56. })
  57. it.each(['?download=1#section', '#section?download=1', '?ignored=%E0%A4%A', '#ignored=%ZZ'])(
  58. 'ignores the literal suffix %s without consuming encoded filename characters', (suffix) => {
  59. expect(parseFileAddress(`dsh-resource://file/session/s/dir/../a%3Fb%23c%25.txt${suffix}`))
  60. .toEqual({ scope: 'session', sessionId: 's', path: 'dir/../a?b#c%.txt' })
  61. expect(parseFileAddress(`dsh-resource://file/absolute/workspace/../a%3Fb%23c%25.txt${suffix}`))
  62. .toEqual({ scope: 'absolute', path: '/workspace/../a?b#c%.txt' })
  63. },
  64. )
  65. it('round-trips an absolute POSIX path with the leading slash dropped', () => {
  66. const address = absoluteFileAddress('/home/me/notes.md')
  67. expect(address).toBe('dsh-resource://file/absolute/home/me/notes.md')
  68. expect(parseFileAddress(address)).toEqual({ scope: 'absolute', path: '/home/me/notes.md' })
  69. })
  70. it('round-trips an absolute Windows drive path with backslashes normalized and the colon literal', () => {
  71. const address = absoluteFileAddress('C:\\w\\x.ts')
  72. expect(address).toBe('dsh-resource://file/absolute/C:/w/x.ts')
  73. expect(parseFileAddress(address)).toEqual({ scope: 'absolute', path: 'C:/w/x.ts' })
  74. })
  75. it('keeps a UNC path\'s identity in an absolute address', () => {
  76. const address = absoluteFileAddress('\\\\server\\share\\x.ts')
  77. expect(address).toBe('dsh-resource://file/absolute//server/share/x.ts')
  78. expect(parseFileAddress(address)).toEqual({ scope: 'absolute', path: '//server/share/x.ts' })
  79. })
  80. it('decodes either spelling of a path segment', () => {
  81. expect(parseFileAddress('dsh-resource://file/session/s/w/a+b.txt')?.path).toBe('w/a+b.txt')
  82. expect(parseFileAddress('dsh-resource://file/session/s/w/a%2Bb.txt')?.path).toBe('w/a+b.txt')
  83. expect(parseFileAddress('dsh-resource://file/absolute/w/a%2Bb.txt')?.path).toBe('/w/a+b.txt')
  84. })
  85. it.each([
  86. ['another resource type', 'dsh-resource://terminal/session/s/1'],
  87. ['a type spelled with another case', 'dsh-resource://File/session/s/w/x.ts'],
  88. ['a scheme spelled with another case', 'DSH-RESOURCE://file/session/s/w/x.ts'],
  89. ['an unknown scope', 'dsh-resource://file/shared/s/w/x'],
  90. ['the retired file:// grammar', 'file://sessions/s/w/x.ts'],
  91. ['a host-less file URL', 'file:///w/x.ts'],
  92. ['a session address with no path', 'dsh-resource://file/session/s'],
  93. ['a session address with no id', 'dsh-resource://file/session'],
  94. ['a session address with an empty id', 'dsh-resource://file/session//a.txt'],
  95. ['an absolute address with no path', 'dsh-resource://file/absolute'],
  96. ['an absolute address with an empty path', 'dsh-resource://file/absolute/'],
  97. ['a UNC marker with no host behind it', 'dsh-resource://file/absolute//'],
  98. ['no scope', 'dsh-resource://file'],
  99. ['another scheme', 'sidebar://files'],
  100. ['not a URL', 'notes.txt'],
  101. ['a malformed escape', 'dsh-resource://file/session/s/%E0%A4%A'],
  102. ['a malformed id escape', 'dsh-resource://file/session/%/a.txt'],
  103. ['a malformed absolute escape', 'dsh-resource://file/absolute/a%ZZ.txt'],
  104. ])('rejects %s', (_, address) => {
  105. expect(parseFileAddress(address)).toBeUndefined()
  106. })
  107. })