isomorphic-git 1.38.5 → 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.
@@ -2074,7 +2074,13 @@ class GitRefManager {
2074
2074
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
2075
2075
  * @returns {Promise<string>} - The resolved object ID.
2076
2076
  */
2077
- 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
+ }) {
2078
2084
  if (depth !== undefined) {
2079
2085
  depth--;
2080
2086
  if (depth === -1) {
@@ -2085,7 +2091,7 @@ class GitRefManager {
2085
2091
  // Is it a ref pointer?
2086
2092
  if (ref.startsWith('ref: ')) {
2087
2093
  ref = ref.slice('ref: '.length);
2088
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
2094
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
2089
2095
  }
2090
2096
  // Is it a complete and valid SHA?
2091
2097
  if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -2104,7 +2110,21 @@ class GitRefManager {
2104
2110
  packedMap.get(ref)
2105
2111
  );
2106
2112
  if (sha) {
2107
- 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
+ })
2108
2128
  }
2109
2129
  }
2110
2130
  // Do we give up?
@@ -3619,21 +3639,26 @@ function applyDelta(delta, source) {
3619
3639
  if (firstOp.byteLength === targetSize) {
3620
3640
  target = firstOp;
3621
3641
  } else {
3622
- // Otherwise, allocate a fresh buffer and slices
3623
- target = Buffer.alloc(targetSize);
3624
- const writer = new BufferCursor(target);
3625
- 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;
3626
3649
 
3627
3650
  while (!reader.eof()) {
3628
- writer.copy(readOp(reader, source));
3651
+ const op = readOp(reader, source);
3652
+ chunks.push(op);
3653
+ tell += op.byteLength;
3629
3654
  }
3630
3655
 
3631
- const tell = writer.tell();
3632
3656
  if (targetSize !== tell) {
3633
3657
  throw new InternalError(
3634
3658
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
3635
3659
  )
3636
3660
  }
3661
+ target = Buffer.concat(chunks, targetSize);
3637
3662
  }
3638
3663
  return target
3639
3664
  }
@@ -4890,8 +4915,25 @@ function parseBuffer(buffer) {
4890
4915
  const type = mode2type$1(mode);
4891
4916
  const path = buffer.slice(space + 1, nullchar).toString('utf8');
4892
4917
 
4893
- // Prevent malicious git repos from writing to "..\foo" on clone etc
4894
- 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
+ ) {
4895
4937
  throw new UnsafeFilepathError(path)
4896
4938
  }
4897
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
@@ -2067,7 +2067,13 @@ class GitRefManager {
2067
2067
  * @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
2068
2068
  * @returns {Promise<string>} - The resolved object ID.
2069
2069
  */
2070
- 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
+ }) {
2071
2077
  if (depth !== undefined) {
2072
2078
  depth--;
2073
2079
  if (depth === -1) {
@@ -2078,7 +2084,7 @@ class GitRefManager {
2078
2084
  // Is it a ref pointer?
2079
2085
  if (ref.startsWith('ref: ')) {
2080
2086
  ref = ref.slice('ref: '.length);
2081
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
2087
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
2082
2088
  }
2083
2089
  // Is it a complete and valid SHA?
2084
2090
  if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -2097,7 +2103,21 @@ class GitRefManager {
2097
2103
  packedMap.get(ref)
2098
2104
  );
2099
2105
  if (sha) {
2100
- 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
+ })
2101
2121
  }
2102
2122
  }
2103
2123
  // Do we give up?
@@ -3612,21 +3632,26 @@ function applyDelta(delta, source) {
3612
3632
  if (firstOp.byteLength === targetSize) {
3613
3633
  target = firstOp;
3614
3634
  } else {
3615
- // Otherwise, allocate a fresh buffer and slices
3616
- target = Buffer.alloc(targetSize);
3617
- const writer = new BufferCursor(target);
3618
- 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;
3619
3642
 
3620
3643
  while (!reader.eof()) {
3621
- writer.copy(readOp(reader, source));
3644
+ const op = readOp(reader, source);
3645
+ chunks.push(op);
3646
+ tell += op.byteLength;
3622
3647
  }
3623
3648
 
3624
- const tell = writer.tell();
3625
3649
  if (targetSize !== tell) {
3626
3650
  throw new InternalError(
3627
3651
  `applyDelta expected target buffer to be ${targetSize} bytes but the resulting buffer was ${tell} bytes`
3628
3652
  )
3629
3653
  }
3654
+ target = Buffer.concat(chunks, targetSize);
3630
3655
  }
3631
3656
  return target
3632
3657
  }
@@ -4877,8 +4902,25 @@ function parseBuffer(buffer) {
4877
4902
  const type = mode2type$1(mode);
4878
4903
  const path = buffer.slice(space + 1, nullchar).toString('utf8');
4879
4904
 
4880
- // Prevent malicious git repos from writing to "..\foo" on clone etc
4881
- 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
+ ) {
4882
4924
  throw new UnsafeFilepathError(path)
4883
4925
  }
4884
4926