isomorphic-git 1.38.4 → 1.38.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -698,6 +698,18 @@ function dirname(path) {
698
698
  * This code for `path.join` is directly copied from @zenfs/core/path for bundle size improvements.
699
699
  * SPDX-License-Identifier: LGPL-3.0-or-later
700
700
  * Copyright (c) James Prevett and other ZenFS contributors.
701
+ *
702
+ * Windows support added:
703
+ * - Backslashes are normalised to forward slashes before processing.
704
+ * - Drive-letter prefixes (e.g. "C:") are detected and preserved through
705
+ * normalisation, so absolute Windows paths are handled correctly.
706
+ * - An absolute argument passed to join() resets the accumulated path,
707
+ * matching Node behaviour and handling worktree gitdir paths properly.
708
+ *
709
+ * Limitation: UNC paths (e.g. \\server\share) are not supported. The leading
710
+ * backslashes are normalised to forward slashes and then collapsed by
711
+ * normalizeString, losing the UNC root. Git on Windows works with
712
+ * drive-letter paths, so this is not expected to be a practical issue.
701
713
  */
702
714
 
703
715
  function normalizeString(path, aar) {
@@ -761,29 +773,67 @@ function normalizeString(path, aar) {
761
773
  return res
762
774
  }
763
775
 
776
+ // Returns the Windows drive prefix ("C:") if present, otherwise null.
777
+ function getWindowsDrivePrefix(path) {
778
+ if (path.length >= 2 && /^[a-zA-Z]:/.test(path)) {
779
+ return path.slice(0, 2) // e.g. "C:"
780
+ }
781
+ return null
782
+ }
783
+
764
784
  function normalize(path) {
765
785
  if (!path.length) return '.'
766
786
 
767
- const isAbsolute = path[0] === '/';
787
+ // Normalise backslashes to forward slashes before any other processing.
788
+ path = path.replace(/\\/g, '/');
789
+
790
+ const drivePrefix = getWindowsDrivePrefix(path);
791
+ // isAbsolute: Unix root ('/foo') OR Windows drive+slash ('C:/foo').
792
+ const isAbsolute =
793
+ path[0] === '/' || (drivePrefix !== null && path[2] === '/');
768
794
  const trailingSeparator = path.at(-1) === '/';
769
795
 
770
- path = normalizeString(path, !isAbsolute);
796
+ // Strip the drive prefix before feeding into normalizeString so that the
797
+ // core algorithm only ever sees a plain POSIX-style string.
798
+ const pathBody = drivePrefix ? path.slice(2) : path;
799
+
800
+ let normalized = normalizeString(pathBody, !isAbsolute);
771
801
 
772
- if (!path.length) {
773
- if (isAbsolute) return '/'
774
- return trailingSeparator ? './' : '.'
802
+ if (!normalized.length) {
803
+ const root = drivePrefix
804
+ ? isAbsolute
805
+ ? drivePrefix + '/'
806
+ : drivePrefix
807
+ : isAbsolute
808
+ ? '/'
809
+ : '.';
810
+ return trailingSeparator && !isAbsolute ? root + '/' : root
775
811
  }
776
- if (trailingSeparator) path += '/';
812
+ if (trailingSeparator) normalized += '/';
777
813
 
778
- return isAbsolute ? `/${path}` : path
814
+ if (drivePrefix) {
815
+ return isAbsolute
816
+ ? `${drivePrefix}/${normalized}`
817
+ : `${drivePrefix}${normalized}`
818
+ }
819
+ return isAbsolute ? `/${normalized}` : normalized
779
820
  }
780
821
 
781
822
  function join(...args) {
782
823
  if (args.length === 0) return '.'
783
824
  let joined;
784
825
  for (let i = 0; i < args.length; ++i) {
785
- const arg = args[i];
786
- if (arg.length > 0) {
826
+ // Normalise separators before processing.
827
+ const arg = args[i].replace(/\\/g, '/');
828
+ if (arg.length === 0) continue
829
+
830
+ // A Windows drive-letter path (e.g. "C:/worktrees/foo") cannot be
831
+ // meaningfully appended to any base, so it resets the accumulator.
832
+ // Unix absolute paths (leading '/') are NOT reset here — that would be
833
+ // path.resolve() semantics; path.join('foo', '/bar') must yield 'foo/bar'.
834
+ if (/^[a-zA-Z]:\//.test(arg)) {
835
+ joined = arg;
836
+ } else {
787
837
  if (joined === undefined) joined = arg;
788
838
  else joined += '/' + arg;
789
839
  }
@@ -2024,7 +2074,13 @@ class GitRefManager {
2024
2074
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
2025
2075
  * @returns {Promise<string>} - The resolved object ID.
2026
2076
  */
2027
- static async resolve({ fs, gitdir, ref, depth = undefined }) {
2077
+ static async resolve({
2078
+ fs,
2079
+ gitdir,
2080
+ ref,
2081
+ depth = undefined,
2082
+ visited = new Set(),
2083
+ }) {
2028
2084
  if (depth !== undefined) {
2029
2085
  depth--;
2030
2086
  if (depth === -1) {
@@ -2035,7 +2091,7 @@ class GitRefManager {
2035
2091
  // Is it a ref pointer?
2036
2092
  if (ref.startsWith('ref: ')) {
2037
2093
  ref = ref.slice('ref: '.length);
2038
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
2094
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
2039
2095
  }
2040
2096
  // Is it a complete and valid SHA?
2041
2097
  if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -2054,7 +2110,21 @@ class GitRefManager {
2054
2110
  packedMap.get(ref)
2055
2111
  );
2056
2112
  if (sha) {
2057
- return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
2113
+ // Guard against circular symbolic references (e.g. a -> b -> a). Without
2114
+ // tracking visited refs this recursion would not terminate.
2115
+ if (visited.has(ref)) {
2116
+ throw new InternalError(
2117
+ `Circular reference detected while resolving ref "${ref}"`
2118
+ )
2119
+ }
2120
+ visited.add(ref);
2121
+ return GitRefManager.resolve({
2122
+ fs,
2123
+ gitdir,
2124
+ ref: sha.trim(),
2125
+ depth,
2126
+ visited,
2127
+ })
2058
2128
  }
2059
2129
  }
2060
2130
  // Do we give up?
@@ -3569,21 +3639,26 @@ function applyDelta(delta, source) {
3569
3639
  if (firstOp.byteLength === targetSize) {
3570
3640
  target = firstOp;
3571
3641
  } else {
3572
- // Otherwise, allocate a fresh buffer and slices
3573
- target = Buffer.alloc(targetSize);
3574
- const writer = new BufferCursor(target);
3575
- writer.copy(firstOp);
3642
+ // Build the result from the delta ops instead of pre-allocating `targetSize`.
3643
+ // `targetSize` comes from the delta header and may not match the actual op output,
3644
+ // so allocating it up front can reserve far more memory than the ops produce.
3645
+ // Building from the ops bounds memory to the real output size; the size check below
3646
+ // still validates the total before concatenating.
3647
+ const chunks = [firstOp];
3648
+ let tell = firstOp.byteLength;
3576
3649
 
3577
3650
  while (!reader.eof()) {
3578
- writer.copy(readOp(reader, source));
3651
+ const op = readOp(reader, source);
3652
+ chunks.push(op);
3653
+ tell += op.byteLength;
3579
3654
  }
3580
3655
 
3581
- const tell = writer.tell();
3582
3656
  if (targetSize !== tell) {
3583
3657
  throw new InternalError(
3584
3658
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
3585
3659
  )
3586
3660
  }
3661
+ target = Buffer.concat(chunks, targetSize);
3587
3662
  }
3588
3663
  return target
3589
3664
  }
@@ -4840,8 +4915,25 @@ function parseBuffer(buffer) {
4840
4915
  const type = mode2type$1(mode);
4841
4916
  const path = buffer.slice(space + 1, nullchar).toString('utf8');
4842
4917
 
4843
- // Prevent malicious git repos from writing to "..\foo" on clone etc
4844
- if (path.includes('\\') || path.includes('/')) {
4918
+ // Reject reserved entry names, matching git's verify_path(): path separators,
4919
+ // "." and ".." (which resolve outside the working directory), and ".git" (which git
4920
+ // disallows as a path component). Checks mirror git's is_ntfs_dotgit() and
4921
+ // is_hfs_dotgit(): case-insensitive, trailing dots/spaces stripped (NTFS),
4922
+ // NTFS 8.3 short name aliases (git~1..git~9), and HFS+ ignorable Unicode
4923
+ // characters stripped (zero-width joiners, directional marks, BOM, etc.).
4924
+ const hfsClean = path.replace(
4925
+ /[\u200C-\u200F\u202A-\u202E\u206A-\u206F\uFEFF]/g,
4926
+ ''
4927
+ );
4928
+ const normalized = hfsClean.toLowerCase().replace(/[. ]+$/, '');
4929
+ if (
4930
+ path.includes('\\') ||
4931
+ path.includes('/') ||
4932
+ hfsClean === '.' ||
4933
+ hfsClean === '..' ||
4934
+ normalized === '.git' ||
4935
+ /^\.?git~[1-9]$/.test(normalized)
4936
+ ) {
4845
4937
  throw new UnsafeFilepathError(path)
4846
4938
  }
4847
4939
 
@@ -568,7 +568,7 @@ export class GitRefManager {
568
568
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
569
569
  * @returns {Promise<string>} - The resolved object ID.
570
570
  */
571
- static resolve({ fs, gitdir, ref, depth }: {
571
+ static resolve({ fs, gitdir, ref, depth, visited, }: {
572
572
  fs: FSClient;
573
573
  gitdir?: string | undefined;
574
574
  ref: string;
@@ -567,7 +567,7 @@ export class GitRefManager {
567
567
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
568
568
  * @returns {Promise<string>} - The resolved object ID.
569
569
  */
570
- static resolve({ fs, gitdir, ref, depth }: {
570
+ static resolve({ fs, gitdir, ref, depth, visited, }: {
571
571
  fs: FSClient;
572
572
  gitdir?: string | undefined;
573
573
  ref: string;
package/managers/index.js CHANGED
@@ -691,6 +691,18 @@ function dirname(path) {
691
691
  * This code for `path.join` is directly copied from @zenfs/core/path for bundle size improvements.
692
692
  * SPDX-License-Identifier: LGPL-3.0-or-later
693
693
  * Copyright (c) James Prevett and other ZenFS contributors.
694
+ *
695
+ * Windows support added:
696
+ * - Backslashes are normalised to forward slashes before processing.
697
+ * - Drive-letter prefixes (e.g. "C:") are detected and preserved through
698
+ * normalisation, so absolute Windows paths are handled correctly.
699
+ * - An absolute argument passed to join() resets the accumulated path,
700
+ * matching Node behaviour and handling worktree gitdir paths properly.
701
+ *
702
+ * Limitation: UNC paths (e.g. \\server\share) are not supported. The leading
703
+ * backslashes are normalised to forward slashes and then collapsed by
704
+ * normalizeString, losing the UNC root. Git on Windows works with
705
+ * drive-letter paths, so this is not expected to be a practical issue.
694
706
  */
695
707
 
696
708
  function normalizeString(path, aar) {
@@ -754,29 +766,67 @@ function normalizeString(path, aar) {
754
766
  return res
755
767
  }
756
768
 
769
+ // Returns the Windows drive prefix ("C:") if present, otherwise null.
770
+ function getWindowsDrivePrefix(path) {
771
+ if (path.length >= 2 && /^[a-zA-Z]:/.test(path)) {
772
+ return path.slice(0, 2) // e.g. "C:"
773
+ }
774
+ return null
775
+ }
776
+
757
777
  function normalize(path) {
758
778
  if (!path.length) return '.'
759
779
 
760
- const isAbsolute = path[0] === '/';
780
+ // Normalise backslashes to forward slashes before any other processing.
781
+ path = path.replace(/\\/g, '/');
782
+
783
+ const drivePrefix = getWindowsDrivePrefix(path);
784
+ // isAbsolute: Unix root ('/foo') OR Windows drive+slash ('C:/foo').
785
+ const isAbsolute =
786
+ path[0] === '/' || (drivePrefix !== null && path[2] === '/');
761
787
  const trailingSeparator = path.at(-1) === '/';
762
788
 
763
- path = normalizeString(path, !isAbsolute);
789
+ // Strip the drive prefix before feeding into normalizeString so that the
790
+ // core algorithm only ever sees a plain POSIX-style string.
791
+ const pathBody = drivePrefix ? path.slice(2) : path;
792
+
793
+ let normalized = normalizeString(pathBody, !isAbsolute);
764
794
 
765
- if (!path.length) {
766
- if (isAbsolute) return '/'
767
- return trailingSeparator ? './' : '.'
795
+ if (!normalized.length) {
796
+ const root = drivePrefix
797
+ ? isAbsolute
798
+ ? drivePrefix + '/'
799
+ : drivePrefix
800
+ : isAbsolute
801
+ ? '/'
802
+ : '.';
803
+ return trailingSeparator && !isAbsolute ? root + '/' : root
768
804
  }
769
- if (trailingSeparator) path += '/';
805
+ if (trailingSeparator) normalized += '/';
770
806
 
771
- return isAbsolute ? `/${path}` : path
807
+ if (drivePrefix) {
808
+ return isAbsolute
809
+ ? `${drivePrefix}/${normalized}`
810
+ : `${drivePrefix}${normalized}`
811
+ }
812
+ return isAbsolute ? `/${normalized}` : normalized
772
813
  }
773
814
 
774
815
  function join(...args) {
775
816
  if (args.length === 0) return '.'
776
817
  let joined;
777
818
  for (let i = 0; i < args.length; ++i) {
778
- const arg = args[i];
779
- if (arg.length > 0) {
819
+ // Normalise separators before processing.
820
+ const arg = args[i].replace(/\\/g, '/');
821
+ if (arg.length === 0) continue
822
+
823
+ // A Windows drive-letter path (e.g. "C:/worktrees/foo") cannot be
824
+ // meaningfully appended to any base, so it resets the accumulator.
825
+ // Unix absolute paths (leading '/') are NOT reset here — that would be
826
+ // path.resolve() semantics; path.join('foo', '/bar') must yield 'foo/bar'.
827
+ if (/^[a-zA-Z]:\//.test(arg)) {
828
+ joined = arg;
829
+ } else {
780
830
  if (joined === undefined) joined = arg;
781
831
  else joined += '/' + arg;
782
832
  }
@@ -2017,7 +2067,13 @@ class GitRefManager {
2017
2067
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
2018
2068
  * @returns {Promise<string>} - The resolved object ID.
2019
2069
  */
2020
- static async resolve({ fs, gitdir, ref, depth = undefined }) {
2070
+ static async resolve({
2071
+ fs,
2072
+ gitdir,
2073
+ ref,
2074
+ depth = undefined,
2075
+ visited = new Set(),
2076
+ }) {
2021
2077
  if (depth !== undefined) {
2022
2078
  depth--;
2023
2079
  if (depth === -1) {
@@ -2028,7 +2084,7 @@ class GitRefManager {
2028
2084
  // Is it a ref pointer?
2029
2085
  if (ref.startsWith('ref: ')) {
2030
2086
  ref = ref.slice('ref: '.length);
2031
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
2087
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
2032
2088
  }
2033
2089
  // Is it a complete and valid SHA?
2034
2090
  if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -2047,7 +2103,21 @@ class GitRefManager {
2047
2103
  packedMap.get(ref)
2048
2104
  );
2049
2105
  if (sha) {
2050
- return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
2106
+ // Guard against circular symbolic references (e.g. a -> b -> a). Without
2107
+ // tracking visited refs this recursion would not terminate.
2108
+ if (visited.has(ref)) {
2109
+ throw new InternalError(
2110
+ `Circular reference detected while resolving ref "${ref}"`
2111
+ )
2112
+ }
2113
+ visited.add(ref);
2114
+ return GitRefManager.resolve({
2115
+ fs,
2116
+ gitdir,
2117
+ ref: sha.trim(),
2118
+ depth,
2119
+ visited,
2120
+ })
2051
2121
  }
2052
2122
  }
2053
2123
  // Do we give up?
@@ -3562,21 +3632,26 @@ function applyDelta(delta, source) {
3562
3632
  if (firstOp.byteLength === targetSize) {
3563
3633
  target = firstOp;
3564
3634
  } else {
3565
- // Otherwise, allocate a fresh buffer and slices
3566
- target = Buffer.alloc(targetSize);
3567
- const writer = new BufferCursor(target);
3568
- writer.copy(firstOp);
3635
+ // Build the result from the delta ops instead of pre-allocating `targetSize`.
3636
+ // `targetSize` comes from the delta header and may not match the actual op output,
3637
+ // so allocating it up front can reserve far more memory than the ops produce.
3638
+ // Building from the ops bounds memory to the real output size; the size check below
3639
+ // still validates the total before concatenating.
3640
+ const chunks = [firstOp];
3641
+ let tell = firstOp.byteLength;
3569
3642
 
3570
3643
  while (!reader.eof()) {
3571
- writer.copy(readOp(reader, source));
3644
+ const op = readOp(reader, source);
3645
+ chunks.push(op);
3646
+ tell += op.byteLength;
3572
3647
  }
3573
3648
 
3574
- const tell = writer.tell();
3575
3649
  if (targetSize !== tell) {
3576
3650
  throw new InternalError(
3577
3651
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
3578
3652
  )
3579
3653
  }
3654
+ target = Buffer.concat(chunks, targetSize);
3580
3655
  }
3581
3656
  return target
3582
3657
  }
@@ -4827,8 +4902,25 @@ function parseBuffer(buffer) {
4827
4902
  const type = mode2type$1(mode);
4828
4903
  const path = buffer.slice(space + 1, nullchar).toString('utf8');
4829
4904
 
4830
- // Prevent malicious git repos from writing to "..\foo" on clone etc
4831
- if (path.includes('\\') || path.includes('/')) {
4905
+ // Reject reserved entry names, matching git's verify_path(): path separators,
4906
+ // "." and ".." (which resolve outside the working directory), and ".git" (which git
4907
+ // disallows as a path component). Checks mirror git's is_ntfs_dotgit() and
4908
+ // is_hfs_dotgit(): case-insensitive, trailing dots/spaces stripped (NTFS),
4909
+ // NTFS 8.3 short name aliases (git~1..git~9), and HFS+ ignorable Unicode
4910
+ // characters stripped (zero-width joiners, directional marks, BOM, etc.).
4911
+ const hfsClean = path.replace(
4912
+ /[\u200C-\u200F\u202A-\u202E\u206A-\u206F\uFEFF]/g,
4913
+ ''
4914
+ );
4915
+ const normalized = hfsClean.toLowerCase().replace(/[. ]+$/, '');
4916
+ if (
4917
+ path.includes('\\') ||
4918
+ path.includes('/') ||
4919
+ hfsClean === '.' ||
4920
+ hfsClean === '..' ||
4921
+ normalized === '.git' ||
4922
+ /^\.?git~[1-9]$/.test(normalized)
4923
+ ) {
4832
4924
  throw new UnsafeFilepathError(path)
4833
4925
  }
4834
4926