@feng0531/dsh-vcs 0.5.1 → 0.7.0
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.
- package/README.md +15 -5
- package/branch-switch-host.mjs +35 -0
- package/branch-switch.mjs +193 -0
- package/dist/client.js +2 -3
- package/dist/editor.js +1 -1
- package/dist/frontend.json +1 -1
- package/dist/interface.js +2 -3
- package/dist/svn-host.mjs +1 -1
- package/index.mjs +34 -7
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +5 -2
- package/src/content-search.mjs +74 -0
- package/src/historical-access.mjs +38 -4
- package/vcs.mjs +12 -25
package/dist/svn-host.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHandler as Qe}from"../index.mjs";import{createHash as Ke}from"node:crypto";function ie({left:e={},right:o={},large:r=!1,allowLarge:t=!1,conflicted:i=!1}={}){let n=[e,o],a=n.some(s=>s.binary)?"binary":n.some(s=>s.previewReason==="size-limit")?"size-limit":n.find(s=>s.previewReason)?.previewReason??null;return{reason:a,canLoadLarge:!r&&t&&!i&&a==="size-limit"&&n.every(s=>s.largeEligible!==!1),complete:a===null,limits:{previewBytes:2097152,largeBytes:8388608}}}var _e=9223372036854775807n;function w(e){if(typeof e!="string"||e.length>19||!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("Invalid SVN revision");let o=BigInt(e);if(o>_e)throw new Error("SVN revision exceeds supported range");return o}function X(e){let o=w(e);return o===0n?null:String(o-1n)}function C(e,{snapshot:o,cursor:r=o,limit:t=50}={}){let i=w(o),n=w(r);if(n>i||!Number.isInteger(t)||t<1||t>100||!Array.isArray(e)||e.length>t+1)throw new Error("Invalid SVN page bounds");let a=n+1n;for(let p of e){let l=w(p);if(l>n||l>=a)throw new Error("SVN revisions must descend within cursor");a=l}let s=e.slice(0,t);return{snapshot:o,revisions:s,nextRevision:e.length>t?X(s.at(-1)):null}}function _(e){if(typeof e!="string"||e.length>32768||!e.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(e))throw new Error("Invalid SVN repository path");if(e!=="/"&&e.slice(1).split("/").some(o=>o===""||o==="."||o===".."))throw new Error("Noncanonical SVN repository path");return e}function U(e,o){return _(e),_(o),o==="/"||e===o||e.startsWith(o+"/")}function L(e,o,r){if(_(o),w(r),typeof e!="string"||e.length>32768||/[\x00-\x20\x7f\\?#]/.test(e))throw new Error("Invalid SVN HTTPS root");let t=new URL(e);if(t.protocol!=="https:"||t.username||t.password||t.href!==e)throw new Error("Noncanonical SVN HTTPS root");let i=t.pathname.split("/").slice(1);i.at(-1)===""&&i.pop();for(let p of i){let l=decodeURIComponent(p);if(!p||l==="."||l===".."||/[\x00-\x1f\x7f/\\]/.test(l))throw new Error("Invalid SVN root path")}let n=e.endsWith("/")?e.slice(0,-1):e,a=o==="/"?"":o.split("/").map(p=>encodeURIComponent(p).replace(/[!'()*]/g,l=>"%"+l.charCodeAt(0).toString(16).toUpperCase())).join("/"),s=n+(a||(t.pathname==="/"?"/":""))+"@"+r;if(s.length>32768)throw new Error("SVN target too long");return s}function H({root:e,scope:o,path:r=o,snapshot:t,revision:i}={}){if(w(i)>w(t))throw new Error("SVN selected revision exceeds snapshot");if(!U(r,o))throw new Error("SVN detail path outside scope");let n=L(e,r,t);return{scope:o,revision:i,snapshot:t,args:["log","--xml","--verbose","--stop-on-copy","--non-interactive","--no-auth-cache","--limit","1","-r",i+":"+i,"--",n]}}function $({root:e,scope:o,path:r=o,snapshot:t,cursor:i=t,limit:n=50}={}){if(C([],{snapshot:t,cursor:i,limit:n}),!U(r,o))throw new Error("SVN history path outside scope");let a=L(e,r,t);return{snapshot:t,cursor:i,limit:n,args:["log","--xml","--stop-on-copy","--non-interactive","--no-auth-cache","--limit",String(n+1),"-r",i+":0","--",a]}}function M(e,o){let r=e?.args;if(!Array.isArray(r)||r.length>16||r.some(f=>typeof f!="string"))throw Error("Invalid SVN command");let t=r.at(-1),i=t.lastIndexOf("@"),n=t.slice(i+1);w(n);let a=o.root.endsWith("/")?o.root.slice(0,-1):o.root;L(o.root,"/",n);let s=t.slice(0,i);if(!(s===a||s.startsWith(a+"/")))throw Error("SVN command target outside root");let p=s.slice(a.length),l=p?"/"+p.slice(1).split("/").map(f=>decodeURIComponent(f)).join("/"):"/";if(!U(l,o.scope)||L(o.root,l,n)!==t)throw Error("SVN command target outside scope or noncanonical");let c;if(r[0]==="cat"||r[0]==="proplist"||r[0]==="info"){w(r[r[0]==="cat"?4:5]);let f=r[r[0]==="cat"?4:5];if(f!==n)throw Error("SVN content operative and peg mismatch");c=r[0]==="cat"?["cat","--non-interactive","--no-auth-cache","-r",f,"--",t]:[r[0],"--xml","--non-interactive","--no-auth-cache","-r",f,"--",t]}else if(r[0]==="log")if(r.includes("--verbose")){let f=r[9]?.split(":")[0];c=H({...o,path:l,snapshot:n,revision:f}).args}else{let f=r[8]?.split(":")[0],m=Number(r[6])-1;c=$({...o,path:l,snapshot:n,cursor:f,limit:m}).args}else throw Error("SVN command is not readonly history");if(JSON.stringify(r)!==JSON.stringify(c))throw Error("Unsupported SVN command shape");return{path:l,pegRevision:n,args:[...c]}}function W(e,o){if(!Array.isArray(e)||e.length>1e4)throw new Error("Invalid SVN path nodes");let r=e.map(t=>{let i=["#text","@_action","@_kind","@_copyfrom-path","@_copyfrom-rev","@_text-mods","@_prop-mods"];if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(n=>!i.includes(n)))throw new Error("Invalid SVN path node");for(let n of["@_text-mods","@_prop-mods"])if(Object.hasOwn(t,n)&&!["true","false","unknown"].includes(t[n]))throw new Error("Invalid SVN modification flag");return{...Object.hasOwn(t,"@_text-mods")?{textModified:t["@_text-mods"]}:{},...Object.hasOwn(t,"@_prop-mods")?{propertiesModified:t["@_prop-mods"]}:{},path:t["#text"],action:t["@_action"],kind:t["@_kind"],...Object.hasOwn(t,"@_copyfrom-path")?{copyFromPath:t["@_copyfrom-path"]}:{},...Object.hasOwn(t,"@_copyfrom-rev")?{copyFromRevision:t["@_copyfrom-rev"]}:{}}});return ee(r,o)}function ee(e,{scope:o,revision:r}={}){_(o);let t=w(r);if(!Array.isArray(e)||e.length>1e4||t===0n&&e.length!==0)throw new Error("Invalid SVN changed paths");let i=new Set;return e.map(a=>{if(!a||typeof a!="object"||Array.isArray(a)||Object.keys(a).some(S=>!["path","action","kind","copyFromPath","copyFromRevision","copySourceOutsideScope","textModified","propertiesModified"].includes(S)))throw new Error("Invalid SVN change structure");let{path:s,action:p,kind:l,copyFromPath:c,copyFromRevision:f}=a;if(_(s),i.has(s)||!["A","D","M","R"].includes(p)||!["file","dir","unknown"].includes(l))throw new Error("Invalid SVN change");i.add(s);let m=c!==void 0||f!==void 0;if(m&&(_(c),!["A","R"].includes(p)||w(f)>=t))throw new Error("Invalid SVN copy source");let g=Object.hasOwn(a,"copySourceOutsideScope");if(g&&(a.copySourceOutsideScope!==!0||m||!["A","R"].includes(p)))throw new Error("Invalid SVN redacted copy source");let y={path:s,action:p,kind:l,...g?{copySourceOutsideScope:!0}:{}};for(let S of["textModified","propertiesModified"])if(Object.hasOwn(a,S)){if(!["true","false","unknown"].includes(a[S]))throw new Error("Invalid SVN modification flag");y[S]=a[S]}return m&&(U(c,o)?(y.copyFromPath=c,y.copyFromRevision=f):y.copySourceOutsideScope=!0),y}).filter(a=>U(a.path,o))}function ae(e,{scope:o,revision:r,path:t}={}){if(w(r)===0n)throw new Error("SVN r0 has no change comparison");let i=ee(e,{scope:o,revision:r}).find(s=>s.path===t);if(!i)throw new Error("SVN path is not an authorized changed member");if(i.kind!=="file")throw new Error("Only SVN file changes can be compared");let n=X(r),a=s=>s===null?{empty:!0}:{empty:!1,path:i.path,revision:s,pegRevision:s};return{path:i.path,action:i.action,left:a(i.action==="A"?null:n),right:a(i.action==="D"?null:r)}}import{randomBytes as je}from"node:crypto";function ce(e){if(!e||typeof e!="object")throw Error("Invalid SVN consent identity");let{sessionId:o,cwd:r,repositoryId:t,root:i,uuid:n,scope:a}=e;for(let s of[o,r,t])if(typeof s!="string"||!s||s.length>32768||s.includes("\0"))throw Error("Invalid SVN consent scope");if(L(i,"/","0"),_(a),typeof n!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(n))throw Error("Invalid SVN repository UUID");return JSON.stringify([o,r,t,i,n,a])}function re({now:e=Date.now,ttlMs:o=3e5,maxEntries:r=32,onRevoke:t=()=>{}}={}){if(!Number.isInteger(o)||o<1||o>3e5||!Number.isInteger(r)||r<1||r>32)throw Error("Invalid SVN consent bounds");if(typeof t!="function")throw Error("Invalid SVN revoke listener");let i=new Map,n=s=>{i.delete(s)&&t(s)},a=()=>{for(let[s,p]of i)p.expires<=e()&&n(s)};return{grant(s,{explicit:p=!1}={}){if(p!==!0)throw Error("Explicit SVN network consent required");let l=ce(s);a();for(let[f,m]of i)m.key===l&&n(f);for(;i.size>=r;)n(i.keys().next().value);let c=je(32).toString("hex");return i.set(c,{key:l,sessionId:s.sessionId,expires:e()+o}),c},assert(s,p){a();let l=ce(p),c=i.get(s);if(!c||c.key!==l)throw Object.assign(Error("SVN network consent missing, expired or identity changed"),{code:"vcs/svn-consent-required"})},revokeSession(s){for(let[p,l]of i)l.sessionId===s&&n(p)},revoke(s){n(s)},clear(){for(let s of[...i.keys()])n(s)}}}import{XMLParser as Ue,XMLValidator as Be}from"fast-xml-parser";function O(e){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||Be.validate(e)!==!0)throw Error("Invalid DAV XML");let o=new Ue({preserveOrder:!0,ignoreAttributes:!1,parseTagValue:!1,parseAttributeValue:!1,trimValues:!1}).parse(e),r=0,t=(i,n,a=0)=>{if(a>32)throw Error("DAV XML too deep");let s=Object.create(null),p="";for(let l of i){if(++r>1e4)throw Error("DAV XML too many nodes");if(Object.hasOwn(l,"#text")){p+=l["#text"];continue}let c=Object.keys(l).filter(v=>v!==":@");if(c.length!==1)throw Error("Invalid DAV XML node");let f=c[0];if(f==="?xml"){if(a!==0)throw Error("Unexpected XML declaration");continue}let m={...n},g={};for(let[v,E]of Object.entries(l[":@"]||{}))v==="@_xmlns"?m[""]=E:v.startsWith("@_xmlns:")?m[v.slice(8)]=E:g[v]=E;let y=f.split(":");if(y.length>2)throw Error("Invalid DAV XML name");let S=y.length===2?y[0]:"",x=y.at(-1),N=m[S],I=N==="DAV:"?"D:"+x:N==="http://subversion.tigris.org/xmlns/dav/"?"S:"+x:N==="svn:"?"L:"+x:N==="http://subversion.tigris.org/xmlns/svn/"?"P:"+x:N==="http://subversion.tigris.org/xmlns/custom/"?"C:"+x:null;if(!I)throw Error("Unsupported DAV namespace");let u=["L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I);if(Object.keys(g).length&&(!u||Object.keys(g).some(v=>!["@_node-kind","@_text-mods","@_prop-mods","@_copyfrom-path","@_copyfrom-rev"].includes(v))))throw Error("Unexpected DAV attribute");let d=t(l[f],m,a+1);if(u&&typeof d!="string")throw Error("Invalid DAV path text");let h=u?{...g,"#text":d}:d;if(["D:response","D:propstat","L:log-item","L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I))(s[I]??=[]).push(h);else{if(Object.hasOwn(s,I))throw Error("Duplicate DAV expanded name");s[I]=h}}if(!Object.keys(s).length)return p;if(p.trim())throw Error("Mixed DAV XML content");return s};return t(o,{})}function pe(e,{scope:o,revision:r}={}){w(r);let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV detail structure")};i(t,["L:log-report"]);let n=t["L:log-report"];if(i(n,["L:log-item"]),n["L:log-item"]?.length!==1)throw Error("One DAV detail revision required");let a=n["L:log-item"][0],s={"L:added-path":"A","L:deleted-path":"D","L:modified-path":"M","L:replaced-path":"R"};if(i(a,["D:version-name","D:comment","D:creator-displayname","L:date",...Object.keys(s)]),a["D:version-name"]!==r)throw Error("DAV detail revision mismatch");let p=c=>{let f=a[c]??"";if(typeof f!="string")throw Error("Invalid DAV detail text");return f},l=[];for(let[c,f]of Object.entries(s))for(let m of a[c]??[]){let{"@_node-kind":g,...y}=m;l.push({...y,"@_kind":g??"unknown","@_action":f})}return{revision:r,author:p("D:creator-displayname"),date:p("L:date"),message:p("D:comment"),changes:W(l,{scope:o,revision:r})}}function le(e,o){let r=O(e);if(!r||typeof r!="object"||Object.keys(r).length!==1||!Object.hasOwn(r,"L:log-report"))throw Error("Invalid DAV log root");let t=r["L:log-report"],i;if(t==="")i=[];else{if(!t||typeof t!="object"||Object.keys(t).some(s=>s!=="L:log-item")||!Array.isArray(t["L:log-item"]))throw Error("Invalid DAV log structure");i=t["L:log-item"]}if(i.length>101)throw Error("DAV log entries exceed limit");let n=i.map(s=>{if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).some(l=>!["D:version-name","D:comment","D:creator-displayname","L:date"].includes(l)))throw Error("Unsupported DAV log item");let p=l=>{let c=s[l]??"";if(typeof c!="string")throw Error("Invalid DAV log text");return c};return{revision:p("D:version-name"),message:p("D:comment"),author:p("D:creator-displayname"),date:p("L:date")}}),a=C(n.map(s=>s.revision),o);return{snapshot:a.snapshot,entries:n.slice(0,a.revisions.length),nextRevision:a.nextRevision}}import{XMLParser as Ce,XMLValidator as Me}from"fast-xml-parser";function fe(e,{scope:o,revision:r}={}){if(w(r),typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Me.validate(e)!==!0)throw new Error("Invalid SVN detail XML");let t=new Ce({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:l=>l==="logentry"||l==="path"}).parse(e),i=(l,c)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(f=>!c.includes(f)&&!(f==="#text"&&typeof l[f]=="string"&&/^[ \t\r\n]*$/.test(l[f]))))throw new Error("Invalid SVN detail structure")};if(i(t,["?xml","log"]),i(t.log,["logentry"]),!Array.isArray(t.log.logentry)||t.log.logentry.length!==1)throw new Error("Expected one SVN revision");let n=t.log.logentry[0];if(i(n,["@_revision","author","date","msg","paths"]),n["@_revision"]!==r)throw new Error("SVN detail revision mismatch");let a=l=>{let c=n[l]??"";if(typeof c!="string")throw new Error("Invalid SVN detail text");return c},s=[];n.paths!==void 0&&!(typeof n.paths=="string"&&/^[ \t\r\n]*$/.test(n.paths))&&(i(n.paths,["path"]),s=n.paths.path??[]);let p=W(s,{scope:o,revision:r});return{revision:r,author:a("author"),date:a("date"),message:a("msg"),pathsAvailable:Object.hasOwn(n,"paths"),changes:p}}import{XMLParser as Xe,XMLValidator as Fe}from"fast-xml-parser";function de(e,o){let r=qe(e),t=C(r.map(i=>i.revision),o);return{snapshot:t.snapshot,entries:r.slice(0,t.revisions.length),nextRevision:t.nextRevision}}function qe(e){if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Fe.validate(e)!==!0)throw new Error("Invalid SVN log XML");let r=new Xe({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:n=>n==="logentry"}).parse(e);if(!Object.hasOwn(r,"log")||Object.keys(r).some(n=>n!=="log"&&n!=="?xml"))throw new Error("Invalid SVN log root");let t=r.log;if(typeof t=="string"&&/^[ \t\r\n]*$/.test(t))return[];if(t&&typeof t=="object"&&typeof t["#text"]=="string"&&/^[ \t\r\n]*$/.test(t["#text"])&&delete t["#text"],!t||typeof t!="object"||Object.keys(t).some(n=>n!=="logentry")||!Array.isArray(t.logentry)||t.logentry.length>101)throw new Error("Invalid SVN log entries");let i=new Set;return t.logentry.map(n=>{if(!n||typeof n!="object"||Array.isArray(n)||Object.keys(n).some(p=>!["@_revision","author","date","msg","#text"].includes(p))||"#text"in n&&(typeof n["#text"]!="string"||!/^[ \t\r\n]*$/.test(n["#text"])))throw new Error("Unsupported SVN log entry structure");let a=n["@_revision"];if(w(a),i.has(a))throw new Error("Duplicate SVN revision");i.add(a);let s=p=>{let l=n[p]??"";if(typeof l!="string")throw new Error("Invalid SVN log text");return l};return{revision:a,author:s("author"),date:s("date"),message:s("msg")}})}import{XMLParser as He,XMLValidator as $e}from"fast-xml-parser";function R(e,{root:o}={}){if(L(o,"/","0"),typeof e!="string"||!e||e.length>32768||/[\\?#]/.test(e)||[...e].some(n=>n.charCodeAt(0)<=32||n.charCodeAt(0)===127))throw Error("Invalid DAV href");if(!(e.startsWith("/")&&!e.startsWith("//"))&&!e.startsWith("https://"))throw Error("DAV href must be absolute path or HTTPS URL");let r=new URL(e,new URL(o).origin);if(r.protocol!=="https:"||r.origin!==new URL(o).origin||r.username||r.password||r.search||r.hash)throw Error("DAV href outside authorized origin");if(e.startsWith("https://")&&r.href!==e||e.startsWith("/")&&r.pathname!==e)throw Error("Noncanonical DAV href");let t=r.pathname.split("/").slice(1);t.at(-1)===""&&t.pop();for(let n of t){let a=decodeURIComponent(n);if(!n||a==="."||a===".."||[...a].some(s=>s.charCodeAt(0)<32||s.charCodeAt(0)===127||s==="/"||s.charCodeAt(0)===92))throw Error("Unsafe DAV href path")}let i=new URL(o).pathname.replace(/[/]$/,"");if(i&&r.pathname!==i&&!r.pathname.startsWith(i+"/"))throw Error("DAV href outside repository root");return r.href}function ue(e,{target:o,revision:r,root:t,uuid:i}={}){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||$e.validate(e)!==!0)throw Error("Invalid SVN node kind XML");let a=new He({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:p=>p==="entry"}).parse(e)?.info?.entry;if(!Array.isArray(a)||a.length!==1)throw Error("Expected one SVN node kind entry");let s=a[0];if(s.url!==o||s["@_revision"]!==r||s.repository?.root!==t||s.repository?.uuid!==i)throw Error("SVN node kind identity mismatch");if(!["file","dir"].includes(s["@_kind"]))throw Error("Unknown SVN node kind");return s["@_kind"]}function he(e,{root:o,target:r}={}){let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV node kind structure")};i(t,["D:multistatus"]),i(t["D:multistatus"],["D:response"]);let n=t["D:multistatus"]["D:response"];if(n?.length!==1)throw Error("Expected one DAV node kind response");let a=n[0];i(a,["D:href","D:propstat"]);let s=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("Expected one DAV node kind status");let p=a["D:propstat"][0];if(i(p,["D:status","D:prop"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV node kind unavailable");i(p["D:prop"],["D:resourcetype"]);let l=p["D:prop"]["D:resourcetype"];if(l===""){if(s!==r)throw Error("DAV node kind target mismatch");return"file"}if(i(l,["D:collection"]),l["D:collection"]!=="")throw Error("Unknown DAV node kind");if(s!==r&&s!==r+"/")throw Error("DAV node kind target mismatch");return"dir"}import{XMLParser as We,XMLValidator as ze}from"fast-xml-parser";function me(e,{target:o,allowEmpty:r=!1}={}){if(typeof o!="string"||!o||o.length>32768)throw new Error("Invalid SVN property target");if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||ze.validate(e)!==!0)throw new Error("Invalid SVN property XML");let t=new We({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:c=>c==="target"||c==="property"}).parse(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)&&!(m==="#text"&&typeof c[m]=="string"&&/^[ \t\r\n]*$/.test(c[m]))))throw new Error("Invalid SVN property structure")};if(i(t,["?xml","properties"]),r===!0&&typeof t.properties=="string"&&/^[ \t\r\n]*$/.test(t.properties))return{target:o,names:[],special:!1};i(t.properties,["target"]);let n=t.properties.target;if(!Array.isArray(n)||n.length!==1)throw new Error("Expected one SVN property target");let a=n[0];if(i(a,["@_path","property"]),a["@_path"]!==o)throw new Error("SVN property target mismatch");let s=a.property??[];if(!Array.isArray(s)||s.length>1e4)throw new Error("Too many SVN properties");let p=[],l=new Set;for(let c of s){i(c,["@_name"]);let f=c["@_name"];if(typeof f!="string"||!f||f.length>1024||/[\x00-\x20\x7f]/.test(f)||l.has(f))throw new Error("Invalid SVN property name");l.add(f),p.push(f)}return{target:o,names:p,special:l.has("svn:special")}}var Se=2*1024*1024,ve=15e3,ge=3e5,we=50,Ge=/^[a-f0-9]{64}$/,te=e=>{if(typeof e!="string"||!Ge.test(e))throw Error("Invalid SVN change ID")},oe=(e,o,r)=>{let t=e.changes.find(i=>i.id===o);return r===void 0||t?.kind===r?t:void 0},ye=e=>e.slice(0,e.lastIndexOf("@")),Ye=(e,o,r,t)=>Ke("sha256").update(JSON.stringify([e.root,e.uuid,e.scope,o,r,t])).digest("hex"),Je=(e,o,r)=>{if(!(typeof e=="string"||Buffer.isBuffer(e))||Buffer.byteLength(e)>r)throw Error("SVN transport output exceeds byte limit or has invalid type");return Buffer.isBuffer(e)&&o.args[0]!=="cat"?new TextDecoder("utf-8",{fatal:!0}).decode(e):e},z=(e,o,r)=>({identity:e,token:o,signal:r,maxBytes:Se});function De({resolveIdentity:e,transport:o,now:r=Date.now,timeoutMs:t=ve,transportFormat:i="cli"}={}){if(!["cli","dav"].includes(i))throw Error("Invalid SVN transport format");if(!Number.isInteger(t)||t<1||t>ve)throw Error("Invalid SVN runtime deadline");if(typeof e!="function")throw Error("Local identity resolver required");let n=!1,a=()=>{if(n)throw new DOMException("SVN runtime disposed","AbortError")},s=new Map,p=u=>{for(let d of s.get(u)||[])d.controller.abort(new DOMException("SVN consent revoked","AbortError"))},l=re({now:r}),c=re({now:r,onRevoke:u=>p(u)}),f=typeof o=="function",m=()=>{if(!f)throw Error("SVN remote transport disabled")},g=async(u,d)=>{a(),d?.throwIfAborted();let h=await e(u,d);return a(),d?.throwIfAborted(),{...h,sessionId:u.sessionId,cwd:h.cwd,repositoryId:u.repositoryId}},y=u=>u.revision===void 0?{}:(w(u.revision),{snapshot:u.revision}),S=async(u,d,h)=>{let v=await g(u,h);return c.assert(d,v),v},x=async(u,d,h)=>{h?.throwIfAborted(),c.assert(d,await g(u,h))},N=async(u,d)=>{a(),M(u,d.identity),d.signal?.throwIfAborted(),c.assert(d.token,d.identity);let h=new AbortController,v=()=>h.abort(d.signal.reason);d.signal?.addEventListener("abort",v,{once:!0});let E={controller:h,sessionId:d.identity.sessionId};s.has(d.token)||s.set(d.token,new Set),s.get(d.token).add(E);let D=setTimeout(()=>h.abort(new DOMException("SVN transport deadline exceeded","TimeoutError")),t);try{let b=async()=>{h.signal.throwIfAborted();let T=await g({sessionId:d.identity.sessionId,repositoryId:d.identity.repositoryId},h.signal);c.assert(d.token,T),h.signal.throwIfAborted()},A=await o(u,{...d,timeoutMs:t,signal:h.signal,assertAuthorized:b});return h.signal.throwIfAborted(),Je(A,u,d.maxBytes)}catch(b){throw h.signal.aborted?h.signal.reason:b}finally{let b=s.get(d.token);b?.delete(E),b?.size||s.delete(d.token),clearTimeout(D),d.signal?.removeEventListener("abort",v)}},I={async describe(u,{signal:d}={}){let h=await g(u,d),v=y(h),E=l.grant(h,{explicit:!0});return{...v,offer:E,root:h.root,uuid:h.uuid,scope:h.scope,origin:new URL(h.root).origin,expiresAt:r()+ge,remoteEnabled:f}},async approve(u,{offer:d,explicit:h=!1,signal:v}={}){if(h!==!0)throw Error("Explicit SVN network consent required");let E=await g(u,v);l.assert(d,E),m();let D=y(E),b=c.grant(E,{explicit:!0});return l.revoke(d),{...D,token:b,expiresAt:r()+ge}},async log(u,{token:d,snapshot:h,cursor:v,limit:E,path:D,signal:b}={}){let A=await S(u,d,b);m();let T=$({...A,snapshot:h,cursor:v,limit:E,path:D});b?.throwIfAborted();let V=await N(T,z(A,d,b));return await x(u,d,b),(i==="dav"?le:de)(V,T)},async detail(u,{token:d,snapshot:h,revision:v,signal:E}={}){let D=await S(u,d,E);m();let b=H({...D,snapshot:h,revision:v}),A=await N(b,z(D,d,E));await x(u,d,E);let T=i==="dav"?{...pe(A,b),pathsAvailable:!0}:fe(A,b);return{...T,snapshot:h,changes:T.changes.map(V=>({...V,id:Ye(D,h,v,V)}))}},async compare(u,{token:d,snapshot:h,revision:v,id:E,signal:D}={}){te(E);let b=await I.detail(u,{token:d,snapshot:h,revision:v,signal:D}),A=oe(b,E);if(!A)throw Error("SVN change is not selected revision member");let T=await S(u,d,D),V=ae(b.changes.map(({id:k,...j})=>j),{scope:T.scope,revision:v,path:A.path}),P=async k=>{if(k.empty)return{label:"empty",text:"",absent:!0};let j=await S(u,d,D),B=L(j.root,k.path,k.pegRevision),q=z(j,d,D),Oe=await N({args:["proplist","--xml","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},q);if(await x(u,d,D),me(Oe,{target:ye(B),allowEmpty:!0}).special)throw Error("SVN special files cannot be compared");let Z=await N({args:["cat","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},q);await x(u,d,D);let Q=Buffer.isBuffer(Z)?Z:Buffer.from(Z);if(Q.length>Se)throw Error("SVN text exceeds 2 MiB");if(Q.includes(0))return{label:"r"+k.revision,text:"",binary:!0};let se;try{se=new TextDecoder("utf-8",{fatal:!0}).decode(Q)}catch{return{label:"r"+k.revision,text:"",binary:!0}}return{label:"r"+k.revision,text:se}},G=async k=>{let j=await S(u,d,D),B=L(j.root,k.path,k.pegRevision),q=await N({args:["info","--xml","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},z(j,d,D));return await x(u,d,D),ue(q,{target:ye(B),revision:k.revision,root:j.root,uuid:j.uuid})},F=V.left;A.action==="R"&&!F.empty&&await G(F)==="dir"&&(F={empty:!0});let Y=await P(F),J=await P(V.right);return{path:A.path,revision:v,snapshot:h,left:Y,right:J,binary:!!(Y.binary||J.binary),preview:ie({left:Y,right:J,allowLarge:!1})}},async trace(u,{token:d,snapshot:h,revision:v,id:E,cursor:D,limit:b=we,signal:A}={}){te(E);let T=await I.detail(u,{token:d,snapshot:h,revision:v,signal:A}),V=oe(T,E,"file");if(!V)throw Error("SVN tracing requires selected file member");let P=V.action==="D"?X(v):v;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:b,path:V.path,signal:A}),selectionSnapshot:h,selectionRevision:v,path:V.path,pegRevision:P,stopOnCopy:!0,copySourceOutsideScope:!!V.copySourceOutsideScope}},async copytrace(u,{token:d,snapshot:h,revision:v,id:E,cursor:D,limit:b=we,signal:A}={}){te(E);let T=await I.detail(u,{token:d,snapshot:h,revision:v,signal:A}),V=oe(T,E,"file");if(!V||V.copySourceOutsideScope||V.copyFromPath===void 0||V.copyFromRevision===void 0)throw Error("Selected file has no authorized copy source");let P=V.copyFromRevision;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:b,path:V.copyFromPath,signal:A}),selectionSnapshot:h,selectionRevision:v,path:V.copyFromPath,pegRevision:P,sourceForPath:V.path,stopOnCopy:!0}},async revoke(u,{token:d,signal:h}={}){return await S(u,d,h),c.revoke(d),p(d),{revoked:!0}},dispose(){n=!0,l.clear(),c.clear();for(let u of s.keys())p(u)},revokeSession(u){l.revokeSession(u),c.revokeSession(u);for(let[d,h]of s)[...h].some(v=>v.sessionId===u)&&p(d)}};return I}var ne=(e,o)=>({ok:!1,error:{code:e,message:o,details:{}}}),Ee={revoke:["token"],describe:[],approve:["offer","explicit"],log:["token","snapshot","cursor","limit"],detail:["token","snapshot","revision"],compare:["token","snapshot","revision","id"],trace:["token","snapshot","revision","id","cursor","limit"],copytrace:["token","snapshot","revision","id","cursor","limit"]};function Ze(e,o){if(!o||typeof o!="object"||Array.isArray(o))throw Error("Object payload required");if(Object.keys(o).some(r=>!["sessionId","repositoryId",...Ee[e]].includes(r)))throw Error("Unknown SVN request field");for(let r of["sessionId","repositoryId"])if(typeof o[r]!="string"||!o[r]||o[r].length>1024||o[r].includes("\0"))throw Error("Invalid SVN Session address");if(e==="approve"&&(o.explicit!==!0||typeof o.offer!="string"||!/^[a-f0-9]{64}$/.test(o.offer)))throw Error("Explicit scoped offer required");if(!["describe","approve"].includes(e)){if(typeof o.token!="string"||!/^[a-f0-9]{64}$/.test(o.token))throw Error("SVN consent token required");if(e!=="revoke"&&w(o.snapshot),o.revision!==void 0&&w(o.revision),["detail","compare","trace","copytrace"].includes(e)&&o.revision===void 0)throw Error("Selected SVN revision required");if(o.cursor!==void 0&&w(o.cursor),o.limit!==void 0&&(!Number.isInteger(o.limit)||o.limit<1||o.limit>100))throw Error("Invalid SVN page size");if(["compare","trace","copytrace"].includes(e)&&(typeof o.id!="string"||!/^[a-f0-9]{64}$/.test(o.id)))throw Error("Selected SVN change ID required")}}function be(e,{timeoutMs:o=15e3}={}){if(!Number.isInteger(o)||o<1||o>15e3)throw Error("Invalid SVN RPC deadline");let r=0;return async(t,i,n)=>{let a=t.startsWith("vcs/svn-")?t.slice(8):"";if(!Object.hasOwn(Ee,a))return ne("vcs/not-found","Unknown SVN endpoint");if(a!=="revoke"&&r>=2)return ne("vcs/busy","SVN history requests busy");a!=="revoke"&&r++;let s=new AbortController,p=()=>s.abort(n.reason);n?.addEventListener("abort",p,{once:!0}),n?.aborted&&p();let l=setTimeout(()=>s.abort(new DOMException("SVN operation deadline exceeded","TimeoutError")),o);try{n?.throwIfAborted(),Ze(a,i);let{sessionId:c,repositoryId:f,...m}=i,g=await e[a]({sessionId:c,repositoryId:f},{...m,signal:s.signal});return s.signal.throwIfAborted(),{ok:!0,value:g}}catch(c){let f=n?.aborted||c?.name==="AbortError"?"vcs/cancelled":["vcs/rediscover-required","vcs/svn-consent-required"].includes(c?.code)?c.code:"vcs/operation-failed",m=s.signal.aborted?s.signal.reason:c,g=m instanceof Error?m.message:String(m);return ne(f,g)}finally{clearTimeout(l),n?.removeEventListener("abort",p),a!=="revoke"&&r--}}}var er=new Set(["describe","approve","log","detail","compare","trace","copytrace","revoke"]);function xe(e,o,{transport:r,transportFormat:t="cli",now:i=Date.now,timeoutMs:n=15e3}={}){let a,s=Qe(e,o,4,{now:i,bindSvnIdentityResolver:c=>{a=c}}),p=De({resolveIdentity:a,transport:r,transportFormat:t,now:i,timeoutMs:n}),l=be(p,{timeoutMs:n});return{handle(c,f,m){let g=c.startsWith("vcs/svn-")?c.slice(8):"";return er.has(g)?l(c,f,m):s(c,f,m)},revokeSession:c=>p.revokeSession(c),dispose:()=>p.dispose()}}import{request as rr}from"node:https";function Ae({request:e=rr}={}){return async(o,{origin:r,method:t="GET",body:i="",depth:n,label:a,signal:s,maxBytes:p=2097152,timeoutMs:l=15e3}={})=>{let c=new URL(o);if(c.protocol!=="https:"||c.origin!==r||c.username||c.password||c.hash||c.href!==o||!["GET","PROPFIND","REPORT"].includes(t)||typeof i!="string"||Buffer.byteLength(i)>65536||!Number.isInteger(p)||p<1||p>2097152||!Number.isInteger(l)||l<1||l>15e3)throw Error("Invalid bounded HTTPS request");if(n!==void 0&&(t!=="PROPFIND"||n!==0))throw Error("Invalid DAV depth");if(a!==void 0){if(t!=="PROPFIND"||n!==0)throw Error("Invalid DAV label");w(a)}let f={...n===0?{Depth:"0"}:{},...a!==void 0?{Label:a}:{}};return s?.throwIfAborted(),new Promise((m,g)=>{let y,S,x=null,N=null,I=0,u=[],d=D=>{x??=D,S?.destroy(),y?.destroy()},h=()=>d(s.reason??new DOMException("HTTPS cancelled","AbortError")),v=setTimeout(()=>d(new DOMException("HTTPS deadline exceeded","TimeoutError")),l),E=()=>{clearTimeout(v),s?.removeEventListener("abort",h)};try{y=e(c,{method:t,agent:!1,rejectUnauthorized:!0,maxHeaderSize:16384,headers:{...f,"Accept-Encoding":"identity","Content-Type":"text/xml; charset=utf-8","Content-Length":String(Buffer.byteLength(i))}},D=>{S=D;let b=S.statusCode??0;if(b>=300&&b<400){d(Error("HTTPS redirects forbidden"));return}if(b<200||b>=300){d(Error("HTTPS status "+b));return}if(S.headers["content-encoding"]&&S.headers["content-encoding"]!=="identity"){d(Error("HTTPS compressed responses forbidden"));return}let A=S.headers["content-length"];if(A!==void 0&&(!/^[0-9]+$/.test(A)||Number(A)>p)){d(Error("HTTPS response exceeds byte limit"));return}S.on("data",T=>{I+=T.length,I>p?d(Error("HTTPS response exceeds byte limit")):x||u.push(T)}),S.on("error",d),S.on("end",()=>{if(!S.complete){d(Error("HTTPS response incomplete"));return}N=Buffer.concat(u)})}),y.on("error",D=>{x??=D}),y.once("close",()=>{E(),x?g(x):N!==null?m(N):g(Error("HTTPS response closed before completion"))}),s?.addEventListener("abort",h,{once:!0}),s?.aborted?h():y.end(i)}catch(D){E(),y?.destroy(),g(D)}})}}var tr=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function Ve(e,o){let r=M(e,o),t=r.args;if(t[0]!=="log")throw Error("DAV log report requires readonly log plan");let i=t.includes("--verbose"),n=t[i?9:8].split(":"),a=t[i?7:6],s=r.path==="/"?"":r.path.slice(1),p='<?xml version="1.0" encoding="utf-8"?><S:log-report xmlns:S="svn:" xmlns:D="DAV:"><S:start-revision>'+n[0]+"</S:start-revision><S:end-revision>"+n[1]+"</S:end-revision><S:limit>"+a+"</S:limit><S:strict-node-history/>"+(i?"<S:discover-changed-paths/>":"")+"<S:path>"+tr(s)+"</S:path><S:revprop>svn:author</S:revprop><S:revprop>svn:date</S:revprop><S:revprop>svn:log</S:revprop></S:log-report>";if(Buffer.byteLength(p)>65536)throw Error("DAV request body exceeds limit");return{method:"REPORT",body:p,pegRevision:r.pegRevision,path:r.path,requiresVerifiedBaseline:!0}}function Ie(e,{root:o,uuid:r,target:t}={}){let i=O(e),n=(f,m)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(g=>!m.includes(g)&&!(g==="#text"&&typeof f[g]=="string"&&f[g].trim()==="")))throw Error("Unsupported DAV discovery structure")};n(i,["?xml","D:multistatus"]);let a=i["D:multistatus"];if(n(a,["D:response"]),!Array.isArray(a["D:response"])||a["D:response"].length!==1)throw Error("DAV discovery requires one response");let s=a["D:response"][0];if(n(s,["D:href","D:propstat"]),R(s["D:href"],{root:o})!==t)throw Error("DAV response target mismatch");if(!Array.isArray(s["D:propstat"])||s["D:propstat"].length!==1)throw Error("DAV discovery requires one property status");let p=s["D:propstat"][0];if(n(p,["D:prop","D:status"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV discovery properties unavailable");let l=p["D:prop"];if(n(l,["S:repository-uuid","D:version-controlled-configuration"]),l["S:repository-uuid"]!==r||typeof r!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(r))throw Error("DAV UUID does not match local identity");let c=l["D:version-controlled-configuration"];return n(c,["D:href"]),{uuid:r,vcc:R(c["D:href"],{root:o}),target:t}}function Te(e,{root:o,revision:r}={}){w(r);let t=O(e),i=(f,m)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(g=>!m.includes(g)))throw Error("Invalid DAV baseline structure")};i(t,["D:multistatus"]);let n=t["D:multistatus"];if(i(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV baseline response required");let a=n["D:response"][0];i(a,["D:href","D:propstat"]);let s=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("One DAV baseline status required");let p=a["D:propstat"][0];if(i(p,["D:status","D:prop"]),p["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV baseline status unavailable");let l=p["D:prop"];if(i(l,["D:version-name","D:baseline-collection"]),l["D:version-name"]!==r)throw Error("DAV baseline revision mismatch");i(l["D:baseline-collection"],["D:href"]);let c=R(l["D:baseline-collection"]["D:href"],{root:o});if(!c.endsWith("/"))throw Error("DAV baseline collection must end in slash");return{revision:r,baseline:s,collection:c}}function Ne({request:e,assertAuthorized:o}={}){if(typeof e!="function"||typeof o!="function")throw Error("DAV chain requires transport and authorization");return async(r,{revision:t,signal:i}={})=>{w(t),L(r.root,r.scope,t);let n=r.root,a=new URL(n).origin,s=n.endsWith("/")?n:n+"/",p=async(m,g)=>{i?.throwIfAborted(),await o(r,{signal:i}),i?.throwIfAborted();let y=await e(m,{origin:a,signal:i,...g});if(i?.throwIfAborted(),await o(r,{signal:i}),i?.throwIfAborted(),!Buffer.isBuffer(y)||y.length>2097152)throw Error("Invalid DAV response bytes");return new TextDecoder("utf-8",{fatal:!0}).decode(y)},l=await p(s,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:" xmlns:S="http://subversion.tigris.org/xmlns/dav/"><D:prop><S:repository-uuid/><D:version-controlled-configuration/></D:prop></D:propfind>'}),{vcc:c}=Ie(l,{root:n,target:s,uuid:r.uuid}),f=await p(c,{method:"PROPFIND",depth:0,label:t,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:baseline-collection/><D:version-name/></D:prop></D:propfind>'});return{...Te(f,{root:n,revision:t}),uuid:r.uuid}}}function Le(e,{root:o,target:r}={}){let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV property names structure")};i(t,["D:multistatus"]);let n=t["D:multistatus"];if(i(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV property response required");let a=n["D:response"][0];if(i(a,["D:href","D:propstat"]),R(a["D:href"],{root:o})!==r)throw Error("DAV property target mismatch");if(a["D:propstat"]?.length!==1)throw Error("One DAV property status required");let s=a["D:propstat"][0];if(i(s,["D:status","D:prop"]),s["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV property names unavailable");let p=s["D:prop"];if(p!==""&&(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).length>1e3||Object.entries(p).some(([c,f])=>!/^[DSPC]:[^:]+$/.test(c)||f!=="")))throw Error("DAV property names must be empty");let l=p===""?[]:Object.keys(p);return{names:l,special:l.includes("P:special")}}var K=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""");function ke({request:e}={}){if(typeof e!="function")throw Error("Bounded DAV request required");return async(o,{identity:r,assertAuthorized:t,signal:i}={})=>{let n=M(o,r),a=n.args[0],s=Ne({request:e,assertAuthorized:t}),p=await s(r,{revision:n.pegRevision,signal:i}),l=async(y,S)=>{i?.throwIfAborted(),await t(),i?.throwIfAborted();let x=await e(y,{origin:new URL(r.root).origin,...S,signal:i});if(i?.throwIfAborted(),await t(),i?.throwIfAborted(),!Buffer.isBuffer(x)||x.length>2097152)throw Error("Invalid DAV transport bytes");return x};if(a==="log"){let y=Ve(o,r);return l(p.collection,{method:"REPORT",body:y.body})}let c=n.path.slice(1).split("/").map(y=>encodeURIComponent(y).replace(/[!'()*]/g,S=>"%"+S.charCodeAt(0).toString(16).toUpperCase())).join("/"),f=R(p.collection+c,{root:r.root});if(a==="info"){let y=await l(f,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:resourcetype/></D:prop></D:propfind>'}),S=he(new TextDecoder("utf-8",{fatal:!0}).decode(y),{root:r.root,target:f}),x=n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@"));return'<info><entry kind="'+S+'" revision="'+n.pegRevision+'"><url>'+K(x)+"</url><repository><root>"+K(r.root)+"</root><uuid>"+K(r.uuid)+"</uuid></repository></entry></info>"}let m=await l(f,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:propname/></D:propfind>'}),g=Le(new TextDecoder("utf-8",{fatal:!0}).decode(m),{root:r.root,target:f});if(a==="proplist")return'<properties><target path="'+K(n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@")))+'">'+(g.special?'<property name="svn:special"/>':"")+"</target></properties>";if(g.special)throw Error("SVN special content forbidden");return l(f,{method:"GET"})}}function Jt(e,o){return xe(e,o,{transportFormat:"dav",transport:ke({request:Ae()})})}export{Jt as createSvnHttpsHost};
|
|
1
|
+
import{createHandler as Qe}from"../index.mjs";import{createHash as Ke}from"node:crypto";function ie({left:e={},right:o={},large:r=!1,allowLarge:t=!1,conflicted:i=!1}={}){let n=[e,o],a=n.some(s=>s.binary)?"binary":n.some(s=>s.previewReason==="size-limit")?"size-limit":n.find(s=>s.previewReason)?.previewReason??null;return{reason:a,canLoadLarge:!r&&t&&!i&&a==="size-limit"&&n.every(s=>s.largeEligible!==!1),complete:a===null,limits:{previewBytes:2097152,largeBytes:8388608}}}var _e=9223372036854775807n;function w(e){if(typeof e!="string"||e.length>19||!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("Invalid SVN revision");let o=BigInt(e);if(o>_e)throw new Error("SVN revision exceeds supported range");return o}function X(e){let o=w(e);return o===0n?null:String(o-1n)}function C(e,{snapshot:o,cursor:r=o,limit:t=50}={}){let i=w(o),n=w(r);if(n>i||!Number.isInteger(t)||t<1||t>100||!Array.isArray(e)||e.length>t+1)throw new Error("Invalid SVN page bounds");let a=n+1n;for(let p of e){let l=w(p);if(l>n||l>=a)throw new Error("SVN revisions must descend within cursor");a=l}let s=e.slice(0,t);return{snapshot:o,revisions:s,nextRevision:e.length>t?X(s.at(-1)):null}}function _(e){if(typeof e!="string"||e.length>32768||!e.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(e))throw new Error("Invalid SVN repository path");if(e!=="/"&&e.slice(1).split("/").some(o=>o===""||o==="."||o===".."))throw new Error("Noncanonical SVN repository path");return e}function U(e,o){return _(e),_(o),o==="/"||e===o||e.startsWith(o+"/")}function L(e,o,r){if(_(o),w(r),typeof e!="string"||e.length>32768||/[\x00-\x20\x7f\\?#]/.test(e))throw new Error("Invalid SVN HTTPS root");let t=new URL(e);if(t.protocol!=="https:"||t.username||t.password||t.href!==e)throw new Error("Noncanonical SVN HTTPS root");let i=t.pathname.split("/").slice(1);i.at(-1)===""&&i.pop();for(let p of i){let l=decodeURIComponent(p);if(!p||l==="."||l===".."||/[\x00-\x1f\x7f/\\]/.test(l))throw new Error("Invalid SVN root path")}let n=e.endsWith("/")?e.slice(0,-1):e,a=o==="/"?"":o.split("/").map(p=>encodeURIComponent(p).replace(/[!'()*]/g,l=>"%"+l.charCodeAt(0).toString(16).toUpperCase())).join("/"),s=n+(a||(t.pathname==="/"?"/":""))+"@"+r;if(s.length>32768)throw new Error("SVN target too long");return s}function H({root:e,scope:o,path:r=o,snapshot:t,revision:i}={}){if(w(i)>w(t))throw new Error("SVN selected revision exceeds snapshot");if(!U(r,o))throw new Error("SVN detail path outside scope");let n=L(e,r,t);return{scope:o,revision:i,snapshot:t,args:["log","--xml","--verbose","--stop-on-copy","--non-interactive","--no-auth-cache","--limit","1","-r",i+":"+i,"--",n]}}function $({root:e,scope:o,path:r=o,snapshot:t,cursor:i=t,limit:n=50}={}){if(C([],{snapshot:t,cursor:i,limit:n}),!U(r,o))throw new Error("SVN history path outside scope");let a=L(e,r,t);return{snapshot:t,cursor:i,limit:n,args:["log","--xml","--stop-on-copy","--non-interactive","--no-auth-cache","--limit",String(n+1),"-r",i+":0","--",a]}}function M(e,o){let r=e?.args;if(!Array.isArray(r)||r.length>16||r.some(f=>typeof f!="string"))throw Error("Invalid SVN command");let t=r.at(-1),i=t.lastIndexOf("@"),n=t.slice(i+1);w(n);let a=o.root.endsWith("/")?o.root.slice(0,-1):o.root;L(o.root,"/",n);let s=t.slice(0,i);if(!(s===a||s.startsWith(a+"/")))throw Error("SVN command target outside root");let p=s.slice(a.length),l=p?"/"+p.slice(1).split("/").map(f=>decodeURIComponent(f)).join("/"):"/";if(!U(l,o.scope)||L(o.root,l,n)!==t)throw Error("SVN command target outside scope or noncanonical");let c;if(r[0]==="cat"||r[0]==="proplist"||r[0]==="info"){w(r[r[0]==="cat"?4:5]);let f=r[r[0]==="cat"?4:5];if(f!==n)throw Error("SVN content operative and peg mismatch");c=r[0]==="cat"?["cat","--non-interactive","--no-auth-cache","-r",f,"--",t]:[r[0],"--xml","--non-interactive","--no-auth-cache","-r",f,"--",t]}else if(r[0]==="log")if(r.includes("--verbose")){let f=r[9]?.split(":")[0];c=H({...o,path:l,snapshot:n,revision:f}).args}else{let f=r[8]?.split(":")[0],m=Number(r[6])-1;c=$({...o,path:l,snapshot:n,cursor:f,limit:m}).args}else throw Error("SVN command is not readonly history");if(JSON.stringify(r)!==JSON.stringify(c))throw Error("Unsupported SVN command shape");return{path:l,pegRevision:n,args:[...c]}}function W(e,o){if(!Array.isArray(e)||e.length>1e4)throw new Error("Invalid SVN path nodes");let r=e.map(t=>{let i=["#text","@_action","@_kind","@_copyfrom-path","@_copyfrom-rev","@_text-mods","@_prop-mods"];if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(n=>!i.includes(n)))throw new Error("Invalid SVN path node");for(let n of["@_text-mods","@_prop-mods"])if(Object.hasOwn(t,n)&&!["true","false","unknown"].includes(t[n]))throw new Error("Invalid SVN modification flag");return{...Object.hasOwn(t,"@_text-mods")?{textModified:t["@_text-mods"]}:{},...Object.hasOwn(t,"@_prop-mods")?{propertiesModified:t["@_prop-mods"]}:{},path:t["#text"],action:t["@_action"],kind:t["@_kind"],...Object.hasOwn(t,"@_copyfrom-path")?{copyFromPath:t["@_copyfrom-path"]}:{},...Object.hasOwn(t,"@_copyfrom-rev")?{copyFromRevision:t["@_copyfrom-rev"]}:{}}});return ee(r,o)}function ee(e,{scope:o,revision:r}={}){_(o);let t=w(r);if(!Array.isArray(e)||e.length>1e4||t===0n&&e.length!==0)throw new Error("Invalid SVN changed paths");let i=new Set;return e.map(a=>{if(!a||typeof a!="object"||Array.isArray(a)||Object.keys(a).some(S=>!["path","action","kind","copyFromPath","copyFromRevision","copySourceOutsideScope","textModified","propertiesModified"].includes(S)))throw new Error("Invalid SVN change structure");let{path:s,action:p,kind:l,copyFromPath:c,copyFromRevision:f}=a;if(_(s),i.has(s)||!["A","D","M","R"].includes(p)||!["file","dir","unknown"].includes(l))throw new Error("Invalid SVN change");i.add(s);let m=c!==void 0||f!==void 0;if(m&&(_(c),!["A","R"].includes(p)||w(f)>=t))throw new Error("Invalid SVN copy source");let g=Object.hasOwn(a,"copySourceOutsideScope");if(g&&(a.copySourceOutsideScope!==!0||m||!["A","R"].includes(p)))throw new Error("Invalid SVN redacted copy source");let y={path:s,action:p,kind:l,...g?{copySourceOutsideScope:!0}:{}};for(let S of["textModified","propertiesModified"])if(Object.hasOwn(a,S)){if(!["true","false","unknown"].includes(a[S]))throw new Error("Invalid SVN modification flag");y[S]=a[S]}return m&&(U(c,o)?(y.copyFromPath=c,y.copyFromRevision=f):y.copySourceOutsideScope=!0),y}).filter(a=>U(a.path,o))}function ae(e,{scope:o,revision:r,path:t}={}){if(w(r)===0n)throw new Error("SVN r0 has no change comparison");let i=ee(e,{scope:o,revision:r}).find(s=>s.path===t);if(!i)throw new Error("SVN path is not an authorized changed member");if(i.kind!=="file")throw new Error("Only SVN file changes can be compared");let n=X(r),a=s=>s===null?{empty:!0}:{empty:!1,path:i.path,revision:s,pegRevision:s};return{path:i.path,action:i.action,left:a(i.action==="A"?null:n),right:a(i.action==="D"?null:r)}}import{randomBytes as je}from"node:crypto";function ce(e){if(!e||typeof e!="object")throw Error("Invalid SVN consent identity");let{sessionId:o,cwd:r,repositoryId:t,root:i,uuid:n,scope:a}=e;for(let s of[o,r,t])if(typeof s!="string"||!s||s.length>32768||s.includes("\0"))throw Error("Invalid SVN consent scope");if(L(i,"/","0"),_(a),typeof n!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(n))throw Error("Invalid SVN repository UUID");return JSON.stringify([o,r,t,i,n,a])}function re({now:e=Date.now,ttlMs:o=3e5,maxEntries:r=32,onRevoke:t=()=>{}}={}){if(!Number.isInteger(o)||o<1||o>3e5||!Number.isInteger(r)||r<1||r>32)throw Error("Invalid SVN consent bounds");if(typeof t!="function")throw Error("Invalid SVN revoke listener");let i=new Map,n=s=>{i.delete(s)&&t(s)},a=()=>{for(let[s,p]of i)p.expires<=e()&&n(s)};return{grant(s,{explicit:p=!1}={}){if(p!==!0)throw Error("Explicit SVN network consent required");let l=ce(s);a();for(let[f,m]of i)m.key===l&&n(f);for(;i.size>=r;)n(i.keys().next().value);let c=je(32).toString("hex");return i.set(c,{key:l,sessionId:s.sessionId,expires:e()+o}),c},assert(s,p){a();let l=ce(p),c=i.get(s);if(!c||c.key!==l)throw Object.assign(Error("SVN network consent missing, expired or identity changed"),{code:"vcs/svn-consent-required"})},revokeSession(s){for(let[p,l]of i)l.sessionId===s&&n(p)},revoke(s){n(s)},clear(){for(let s of[...i.keys()])n(s)}}}import{XMLParser as Ue,XMLValidator as Be}from"fast-xml-parser";function O(e){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||Be.validate(e)!==!0)throw Error("Invalid DAV XML");let o=new Ue({preserveOrder:!0,ignoreAttributes:!1,parseTagValue:!1,parseAttributeValue:!1,trimValues:!1}).parse(e),r=0,t=(i,n,a=0)=>{if(a>32)throw Error("DAV XML too deep");let s=Object.create(null),p="";for(let l of i){if(++r>1e4)throw Error("DAV XML too many nodes");if(Object.hasOwn(l,"#text")){p+=l["#text"];continue}let c=Object.keys(l).filter(v=>v!==":@");if(c.length!==1)throw Error("Invalid DAV XML node");let f=c[0];if(f==="?xml"){if(a!==0)throw Error("Unexpected XML declaration");continue}let m={...n},g={};for(let[v,E]of Object.entries(l[":@"]||{}))v==="@_xmlns"?m[""]=E:v.startsWith("@_xmlns:")?m[v.slice(8)]=E:g[v]=E;let y=f.split(":");if(y.length>2)throw Error("Invalid DAV XML name");let S=y.length===2?y[0]:"",x=y.at(-1),N=m[S],I=N==="DAV:"?"D:"+x:N==="http://subversion.tigris.org/xmlns/dav/"?"S:"+x:N==="svn:"?"L:"+x:N==="http://subversion.tigris.org/xmlns/svn/"?"P:"+x:N==="http://subversion.tigris.org/xmlns/custom/"?"C:"+x:null;if(!I)throw Error("Unsupported DAV namespace");let u=["L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I);if(Object.keys(g).length&&(!u||Object.keys(g).some(v=>!["@_node-kind","@_text-mods","@_prop-mods","@_copyfrom-path","@_copyfrom-rev"].includes(v))))throw Error("Unexpected DAV attribute");let d=t(l[f],m,a+1);if(u&&typeof d!="string")throw Error("Invalid DAV path text");let h=u?{...g,"#text":d}:d;if(["D:response","D:propstat","L:log-item","L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I))(s[I]??=[]).push(h);else{if(Object.hasOwn(s,I))throw Error("Duplicate DAV expanded name");s[I]=h}}if(!Object.keys(s).length)return p;if(p.trim())throw Error("Mixed DAV XML content");return s};return t(o,{})}function pe(e,{scope:o,revision:r}={}){w(r);let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV detail structure")};i(t,["L:log-report"]);let n=t["L:log-report"];if(i(n,["L:log-item"]),n["L:log-item"]?.length!==1)throw Error("One DAV detail revision required");let a=n["L:log-item"][0],s={"L:added-path":"A","L:deleted-path":"D","L:modified-path":"M","L:replaced-path":"R"};if(i(a,["D:version-name","D:comment","D:creator-displayname","L:date",...Object.keys(s)]),a["D:version-name"]!==r)throw Error("DAV detail revision mismatch");let p=c=>{let f=a[c]??"";if(typeof f!="string")throw Error("Invalid DAV detail text");return f},l=[];for(let[c,f]of Object.entries(s))for(let m of a[c]??[]){let{"@_node-kind":g,...y}=m;l.push({...y,"@_kind":g??"unknown","@_action":f})}return{revision:r,author:p("D:creator-displayname"),date:p("L:date"),message:p("D:comment"),changes:W(l,{scope:o,revision:r})}}function le(e,o){let r=O(e);if(!r||typeof r!="object"||Object.keys(r).length!==1||!Object.hasOwn(r,"L:log-report"))throw Error("Invalid DAV log root");let t=r["L:log-report"],i;if(t==="")i=[];else{if(!t||typeof t!="object"||Object.keys(t).some(s=>s!=="L:log-item")||!Array.isArray(t["L:log-item"]))throw Error("Invalid DAV log structure");i=t["L:log-item"]}if(i.length>101)throw Error("DAV log entries exceed limit");let n=i.map(s=>{if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).some(l=>!["D:version-name","D:comment","D:creator-displayname","L:date"].includes(l)))throw Error("Unsupported DAV log item");let p=l=>{let c=s[l]??"";if(typeof c!="string")throw Error("Invalid DAV log text");return c};return{revision:p("D:version-name"),message:p("D:comment"),author:p("D:creator-displayname"),date:p("L:date")}}),a=C(n.map(s=>s.revision),o);return{snapshot:a.snapshot,entries:n.slice(0,a.revisions.length),nextRevision:a.nextRevision}}import{XMLParser as Ce,XMLValidator as Me}from"fast-xml-parser";function fe(e,{scope:o,revision:r}={}){if(w(r),typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Me.validate(e)!==!0)throw new Error("Invalid SVN detail XML");let t=new Ce({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:l=>l==="logentry"||l==="path"}).parse(e),i=(l,c)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(f=>!c.includes(f)&&!(f==="#text"&&typeof l[f]=="string"&&/^[ \t\r\n]*$/.test(l[f]))))throw new Error("Invalid SVN detail structure")};if(i(t,["?xml","log"]),i(t.log,["logentry"]),!Array.isArray(t.log.logentry)||t.log.logentry.length!==1)throw new Error("Expected one SVN revision");let n=t.log.logentry[0];if(i(n,["@_revision","author","date","msg","paths"]),n["@_revision"]!==r)throw new Error("SVN detail revision mismatch");let a=l=>{let c=n[l]??"";if(typeof c!="string")throw new Error("Invalid SVN detail text");return c},s=[];n.paths!==void 0&&!(typeof n.paths=="string"&&/^[ \t\r\n]*$/.test(n.paths))&&(i(n.paths,["path"]),s=n.paths.path??[]);let p=W(s,{scope:o,revision:r});return{revision:r,author:a("author"),date:a("date"),message:a("msg"),pathsAvailable:Object.hasOwn(n,"paths"),changes:p}}import{XMLParser as Xe,XMLValidator as Fe}from"fast-xml-parser";function de(e,o){let r=qe(e),t=C(r.map(i=>i.revision),o);return{snapshot:t.snapshot,entries:r.slice(0,t.revisions.length),nextRevision:t.nextRevision}}function qe(e){if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Fe.validate(e)!==!0)throw new Error("Invalid SVN log XML");let r=new Xe({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:n=>n==="logentry"}).parse(e);if(!Object.hasOwn(r,"log")||Object.keys(r).some(n=>n!=="log"&&n!=="?xml"))throw new Error("Invalid SVN log root");let t=r.log;if(typeof t=="string"&&/^[ \t\r\n]*$/.test(t))return[];if(t&&typeof t=="object"&&typeof t["#text"]=="string"&&/^[ \t\r\n]*$/.test(t["#text"])&&delete t["#text"],!t||typeof t!="object"||Object.keys(t).some(n=>n!=="logentry")||!Array.isArray(t.logentry)||t.logentry.length>101)throw new Error("Invalid SVN log entries");let i=new Set;return t.logentry.map(n=>{if(!n||typeof n!="object"||Array.isArray(n)||Object.keys(n).some(p=>!["@_revision","author","date","msg","#text"].includes(p))||"#text"in n&&(typeof n["#text"]!="string"||!/^[ \t\r\n]*$/.test(n["#text"])))throw new Error("Unsupported SVN log entry structure");let a=n["@_revision"];if(w(a),i.has(a))throw new Error("Duplicate SVN revision");i.add(a);let s=p=>{let l=n[p]??"";if(typeof l!="string")throw new Error("Invalid SVN log text");return l};return{revision:a,author:s("author"),date:s("date"),message:s("msg")}})}import{XMLParser as He,XMLValidator as $e}from"fast-xml-parser";function R(e,{root:o}={}){if(L(o,"/","0"),typeof e!="string"||!e||e.length>32768||/[\\?#]/.test(e)||[...e].some(n=>n.charCodeAt(0)<=32||n.charCodeAt(0)===127))throw Error("Invalid DAV href");if(!(e.startsWith("/")&&!e.startsWith("//"))&&!e.startsWith("https://"))throw Error("DAV href must be absolute path or HTTPS URL");let r=new URL(e,new URL(o).origin);if(r.protocol!=="https:"||r.origin!==new URL(o).origin||r.username||r.password||r.search||r.hash)throw Error("DAV href outside authorized origin");if(e.startsWith("https://")&&r.href!==e||e.startsWith("/")&&r.pathname!==e)throw Error("Noncanonical DAV href");let t=r.pathname.split("/").slice(1);t.at(-1)===""&&t.pop();for(let n of t){let a=decodeURIComponent(n);if(!n||a==="."||a===".."||[...a].some(s=>s.charCodeAt(0)<32||s.charCodeAt(0)===127||s==="/"||s.charCodeAt(0)===92))throw Error("Unsafe DAV href path")}let i=new URL(o).pathname.replace(/[/]$/,"");if(i&&r.pathname!==i&&!r.pathname.startsWith(i+"/"))throw Error("DAV href outside repository root");return r.href}function ue(e,{target:o,revision:r,root:t,uuid:i}={}){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||$e.validate(e)!==!0)throw Error("Invalid SVN node kind XML");let a=new He({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:p=>p==="entry"}).parse(e)?.info?.entry;if(!Array.isArray(a)||a.length!==1)throw Error("Expected one SVN node kind entry");let s=a[0];if(s.url!==o||s["@_revision"]!==r||s.repository?.root!==t||s.repository?.uuid!==i)throw Error("SVN node kind identity mismatch");if(!["file","dir"].includes(s["@_kind"]))throw Error("Unknown SVN node kind");return s["@_kind"]}function he(e,{root:o,target:r}={}){let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV node kind structure")};i(t,["D:multistatus"]),i(t["D:multistatus"],["D:response"]);let n=t["D:multistatus"]["D:response"];if(n?.length!==1)throw Error("Expected one DAV node kind response");let a=n[0];i(a,["D:href","D:propstat"]);let s=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("Expected one DAV node kind status");let p=a["D:propstat"][0];if(i(p,["D:status","D:prop"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV node kind unavailable");i(p["D:prop"],["D:resourcetype"]);let l=p["D:prop"]["D:resourcetype"];if(l===""){if(s!==r)throw Error("DAV node kind target mismatch");return"file"}if(i(l,["D:collection"]),l["D:collection"]!=="")throw Error("Unknown DAV node kind");if(s!==r&&s!==r+"/")throw Error("DAV node kind target mismatch");return"dir"}import{XMLParser as We,XMLValidator as ze}from"fast-xml-parser";function me(e,{target:o,allowEmpty:r=!1}={}){if(typeof o!="string"||!o||o.length>32768)throw new Error("Invalid SVN property target");if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||ze.validate(e)!==!0)throw new Error("Invalid SVN property XML");let t=new We({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:c=>c==="target"||c==="property"}).parse(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)&&!(m==="#text"&&typeof c[m]=="string"&&/^[ \t\r\n]*$/.test(c[m]))))throw new Error("Invalid SVN property structure")};if(i(t,["?xml","properties"]),r===!0&&typeof t.properties=="string"&&/^[ \t\r\n]*$/.test(t.properties))return{target:o,names:[],special:!1};i(t.properties,["target"]);let n=t.properties.target;if(!Array.isArray(n)||n.length!==1)throw new Error("Expected one SVN property target");let a=n[0];if(i(a,["@_path","property"]),a["@_path"]!==o)throw new Error("SVN property target mismatch");let s=a.property??[];if(!Array.isArray(s)||s.length>1e4)throw new Error("Too many SVN properties");let p=[],l=new Set;for(let c of s){i(c,["@_name"]);let f=c["@_name"];if(typeof f!="string"||!f||f.length>1024||/[\x00-\x20\x7f]/.test(f)||l.has(f))throw new Error("Invalid SVN property name");l.add(f),p.push(f)}return{target:o,names:p,special:l.has("svn:special")}}var Se=2*1024*1024,ve=15e3,ge=3e5,we=50,Ge=/^[a-f0-9]{64}$/,te=e=>{if(typeof e!="string"||!Ge.test(e))throw Error("Invalid SVN change ID")},oe=(e,o,r)=>{let t=e.changes.find(i=>i.id===o);return r===void 0||t?.kind===r?t:void 0},ye=e=>e.slice(0,e.lastIndexOf("@")),Ye=(e,o,r,t)=>Ke("sha256").update(JSON.stringify([e.root,e.uuid,e.scope,o,r,t])).digest("hex"),Je=(e,o,r)=>{if(!(typeof e=="string"||Buffer.isBuffer(e))||Buffer.byteLength(e)>r)throw Error("SVN transport output exceeds byte limit or has invalid type");return Buffer.isBuffer(e)&&o.args[0]!=="cat"?new TextDecoder("utf-8",{fatal:!0}).decode(e):e},z=(e,o,r)=>({identity:e,token:o,signal:r,maxBytes:Se});function De({resolveIdentity:e,transport:o,now:r=Date.now,timeoutMs:t=ve,transportFormat:i="cli"}={}){if(!["cli","dav"].includes(i))throw Error("Invalid SVN transport format");if(!Number.isInteger(t)||t<1||t>ve)throw Error("Invalid SVN runtime deadline");if(typeof e!="function")throw Error("Local identity resolver required");let n=!1,a=()=>{if(n)throw new DOMException("SVN runtime disposed","AbortError")},s=new Map,p=u=>{for(let d of s.get(u)||[])d.controller.abort(new DOMException("SVN consent revoked","AbortError"))},l=re({now:r}),c=re({now:r,onRevoke:u=>p(u)}),f=typeof o=="function",m=()=>{if(!f)throw Error("SVN remote transport disabled")},g=async(u,d)=>{a(),d?.throwIfAborted();let h=await e(u,d);return a(),d?.throwIfAborted(),{...h,sessionId:u.sessionId,cwd:h.cwd,repositoryId:u.repositoryId}},y=u=>u.revision===void 0?{}:(w(u.revision),{snapshot:u.revision}),S=async(u,d,h)=>{let v=await g(u,h);return c.assert(d,v),v},x=async(u,d,h)=>{h?.throwIfAborted(),c.assert(d,await g(u,h))},N=async(u,d)=>{a(),M(u,d.identity),d.signal?.throwIfAborted(),c.assert(d.token,d.identity);let h=new AbortController,v=()=>h.abort(d.signal.reason);d.signal?.addEventListener("abort",v,{once:!0});let E={controller:h,sessionId:d.identity.sessionId};s.has(d.token)||s.set(d.token,new Set),s.get(d.token).add(E);let D=setTimeout(()=>h.abort(new DOMException("SVN transport deadline exceeded","TimeoutError")),t);try{let b=async()=>{h.signal.throwIfAborted();let T=await g({sessionId:d.identity.sessionId,repositoryId:d.identity.repositoryId},h.signal);c.assert(d.token,T),h.signal.throwIfAborted()},A=await o(u,{...d,timeoutMs:t,signal:h.signal,assertAuthorized:b});return h.signal.throwIfAborted(),Je(A,u,d.maxBytes)}catch(b){throw h.signal.aborted?h.signal.reason:b}finally{let b=s.get(d.token);b?.delete(E),b?.size||s.delete(d.token),clearTimeout(D),d.signal?.removeEventListener("abort",v)}},I={async describe(u,{signal:d}={}){let h=await g(u,d),v=y(h),E=l.grant(h,{explicit:!0});return{...v,offer:E,root:h.root,uuid:h.uuid,scope:h.scope,origin:new URL(h.root).origin,expiresAt:r()+ge,remoteEnabled:f}},async approve(u,{offer:d,explicit:h=!1,signal:v}={}){if(h!==!0)throw Error("Explicit SVN network consent required");let E=await g(u,v);l.assert(d,E),m();let D=y(E),b=c.grant(E,{explicit:!0});return l.revoke(d),{...D,token:b,expiresAt:r()+ge}},async log(u,{token:d,snapshot:h,cursor:v,limit:E,path:D,signal:b}={}){let A=await S(u,d,b);m();let T=$({...A,snapshot:h,cursor:v,limit:E,path:D});b?.throwIfAborted();let V=await N(T,z(A,d,b));return await x(u,d,b),(i==="dav"?le:de)(V,T)},async detail(u,{token:d,snapshot:h,revision:v,signal:E}={}){let D=await S(u,d,E);m();let b=H({...D,snapshot:h,revision:v}),A=await N(b,z(D,d,E));await x(u,d,E);let T=i==="dav"?{...pe(A,b),pathsAvailable:!0}:fe(A,b);return{...T,snapshot:h,changes:T.changes.map(V=>({...V,id:Ye(D,h,v,V)}))}},async compare(u,{token:d,snapshot:h,revision:v,id:E,signal:D}={}){te(E);let b=await I.detail(u,{token:d,snapshot:h,revision:v,signal:D}),A=oe(b,E);if(!A)throw Error("SVN change is not selected revision member");let T=await S(u,d,D),V=ae(b.changes.map(({id:k,...j})=>j),{scope:T.scope,revision:v,path:A.path}),P=async k=>{if(k.empty)return{label:"empty",text:"",absent:!0};let j=await S(u,d,D),B=L(j.root,k.path,k.pegRevision),q=z(j,d,D),Oe=await N({args:["proplist","--xml","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},q);if(await x(u,d,D),me(Oe,{target:ye(B),allowEmpty:!0}).special)throw Error("SVN special files cannot be compared");let Z=await N({args:["cat","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},q);await x(u,d,D);let Q=Buffer.isBuffer(Z)?Z:Buffer.from(Z);if(Q.length>Se)throw Error("SVN text exceeds 2 MiB");if(Q.includes(0))return{label:"r"+k.revision,text:"",binary:!0};let se;try{se=new TextDecoder("utf-8",{fatal:!0}).decode(Q)}catch{return{label:"r"+k.revision,text:"",binary:!0}}return{label:"r"+k.revision,text:se}},G=async k=>{let j=await S(u,d,D),B=L(j.root,k.path,k.pegRevision),q=await N({args:["info","--xml","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},z(j,d,D));return await x(u,d,D),ue(q,{target:ye(B),revision:k.revision,root:j.root,uuid:j.uuid})},F=V.left;A.action==="R"&&!F.empty&&await G(F)==="dir"&&(F={empty:!0});let Y=await P(F),J=await P(V.right);return{path:A.path,revision:v,snapshot:h,left:Y,right:J,binary:!!(Y.binary||J.binary),preview:ie({left:Y,right:J,allowLarge:!1})}},async trace(u,{token:d,snapshot:h,revision:v,id:E,cursor:D,limit:b=we,signal:A}={}){te(E);let T=await I.detail(u,{token:d,snapshot:h,revision:v,signal:A}),V=oe(T,E,"file");if(!V)throw Error("SVN tracing requires selected file member");let P=V.action==="D"?X(v):v;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:b,path:V.path,signal:A}),selectionSnapshot:h,selectionRevision:v,path:V.path,pegRevision:P,stopOnCopy:!0,copySourceOutsideScope:!!V.copySourceOutsideScope}},async copytrace(u,{token:d,snapshot:h,revision:v,id:E,cursor:D,limit:b=we,signal:A}={}){te(E);let T=await I.detail(u,{token:d,snapshot:h,revision:v,signal:A}),V=oe(T,E,"file");if(!V||V.copySourceOutsideScope||V.copyFromPath===void 0||V.copyFromRevision===void 0)throw Error("Selected file has no authorized copy source");let P=V.copyFromRevision;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:b,path:V.copyFromPath,signal:A}),selectionSnapshot:h,selectionRevision:v,path:V.copyFromPath,pegRevision:P,sourceForPath:V.path,stopOnCopy:!0}},async revoke(u,{token:d,signal:h}={}){return await S(u,d,h),c.revoke(d),p(d),{revoked:!0}},dispose(){n=!0,l.clear(),c.clear();for(let u of s.keys())p(u)},revokeSession(u){l.revokeSession(u),c.revokeSession(u);for(let[d,h]of s)[...h].some(v=>v.sessionId===u)&&p(d)}};return I}var ne=(e,o)=>({ok:!1,error:{code:e,message:o,details:{}}}),Ee={revoke:["token"],describe:[],approve:["offer","explicit"],log:["token","snapshot","cursor","limit"],detail:["token","snapshot","revision"],compare:["token","snapshot","revision","id"],trace:["token","snapshot","revision","id","cursor","limit"],copytrace:["token","snapshot","revision","id","cursor","limit"]};function Ze(e,o){if(!o||typeof o!="object"||Array.isArray(o))throw Error("Object payload required");if(Object.keys(o).some(r=>!["sessionId","repositoryId",...Ee[e]].includes(r)))throw Error("Unknown SVN request field");for(let r of["sessionId","repositoryId"])if(typeof o[r]!="string"||!o[r]||o[r].length>1024||o[r].includes("\0"))throw Error("Invalid SVN Session address");if(e==="approve"&&(o.explicit!==!0||typeof o.offer!="string"||!/^[a-f0-9]{64}$/.test(o.offer)))throw Error("Explicit scoped offer required");if(!["describe","approve"].includes(e)){if(typeof o.token!="string"||!/^[a-f0-9]{64}$/.test(o.token))throw Error("SVN consent token required");if(e!=="revoke"&&w(o.snapshot),o.revision!==void 0&&w(o.revision),["detail","compare","trace","copytrace"].includes(e)&&o.revision===void 0)throw Error("Selected SVN revision required");if(o.cursor!==void 0&&w(o.cursor),o.limit!==void 0&&(!Number.isInteger(o.limit)||o.limit<1||o.limit>100))throw Error("Invalid SVN page size");if(["compare","trace","copytrace"].includes(e)&&(typeof o.id!="string"||!/^[a-f0-9]{64}$/.test(o.id)))throw Error("Selected SVN change ID required")}}function be(e,{timeoutMs:o=15e3}={}){if(!Number.isInteger(o)||o<1||o>15e3)throw Error("Invalid SVN RPC deadline");let r=0;return async(t,i,n)=>{let a=t.startsWith("vcs/svn-")?t.slice(8):"";if(!Object.hasOwn(Ee,a))return ne("vcs/not-found","Unknown SVN endpoint");if(a!=="revoke"&&r>=2)return ne("vcs/busy","SVN history requests busy");a!=="revoke"&&r++;let s=new AbortController,p=()=>s.abort(n.reason);n?.addEventListener("abort",p,{once:!0}),n?.aborted&&p();let l=setTimeout(()=>s.abort(new DOMException("SVN operation deadline exceeded","TimeoutError")),o);try{n?.throwIfAborted(),Ze(a,i);let{sessionId:c,repositoryId:f,...m}=i,g=await e[a]({sessionId:c,repositoryId:f},{...m,signal:s.signal});return s.signal.throwIfAborted(),{ok:!0,value:g}}catch(c){let f=n?.aborted||c?.name==="AbortError"?"vcs/cancelled":["vcs/rediscover-required","vcs/svn-consent-required"].includes(c?.code)?c.code:"vcs/operation-failed",m=s.signal.aborted?s.signal.reason:c,g=m instanceof Error?m.message:String(m);return ne(f,g)}finally{clearTimeout(l),n?.removeEventListener("abort",p),a!=="revoke"&&r--}}}var er=new Set(["describe","approve","log","detail","compare","trace","copytrace","revoke"]);function xe(e,o,{transport:r,transportFormat:t="cli",now:i=Date.now,timeoutMs:n=15e3}={}){let a,s=Qe(e,o,4,{now:i,bindSvnIdentityResolver:c=>{a=c}}),p=De({resolveIdentity:a,transport:r,transportFormat:t,now:i,timeoutMs:n}),l=be(p,{timeoutMs:n});return{handle(c,f,m){let g=c.startsWith("vcs/svn-")?c.slice(8):"";return er.has(g)?l(c,f,m):s(c,f,m)},revokeSession:c=>p.revokeSession(c),dispose:()=>(s.dispose?.(),p.dispose())}}import{request as rr}from"node:https";function Ae({request:e=rr}={}){return async(o,{origin:r,method:t="GET",body:i="",depth:n,label:a,signal:s,maxBytes:p=2097152,timeoutMs:l=15e3}={})=>{let c=new URL(o);if(c.protocol!=="https:"||c.origin!==r||c.username||c.password||c.hash||c.href!==o||!["GET","PROPFIND","REPORT"].includes(t)||typeof i!="string"||Buffer.byteLength(i)>65536||!Number.isInteger(p)||p<1||p>2097152||!Number.isInteger(l)||l<1||l>15e3)throw Error("Invalid bounded HTTPS request");if(n!==void 0&&(t!=="PROPFIND"||n!==0))throw Error("Invalid DAV depth");if(a!==void 0){if(t!=="PROPFIND"||n!==0)throw Error("Invalid DAV label");w(a)}let f={...n===0?{Depth:"0"}:{},...a!==void 0?{Label:a}:{}};return s?.throwIfAborted(),new Promise((m,g)=>{let y,S,x=null,N=null,I=0,u=[],d=D=>{x??=D,S?.destroy(),y?.destroy()},h=()=>d(s.reason??new DOMException("HTTPS cancelled","AbortError")),v=setTimeout(()=>d(new DOMException("HTTPS deadline exceeded","TimeoutError")),l),E=()=>{clearTimeout(v),s?.removeEventListener("abort",h)};try{y=e(c,{method:t,agent:!1,rejectUnauthorized:!0,maxHeaderSize:16384,headers:{...f,"Accept-Encoding":"identity","Content-Type":"text/xml; charset=utf-8","Content-Length":String(Buffer.byteLength(i))}},D=>{S=D;let b=S.statusCode??0;if(b>=300&&b<400){d(Error("HTTPS redirects forbidden"));return}if(b<200||b>=300){d(Error("HTTPS status "+b));return}if(S.headers["content-encoding"]&&S.headers["content-encoding"]!=="identity"){d(Error("HTTPS compressed responses forbidden"));return}let A=S.headers["content-length"];if(A!==void 0&&(!/^[0-9]+$/.test(A)||Number(A)>p)){d(Error("HTTPS response exceeds byte limit"));return}S.on("data",T=>{I+=T.length,I>p?d(Error("HTTPS response exceeds byte limit")):x||u.push(T)}),S.on("error",d),S.on("end",()=>{if(!S.complete){d(Error("HTTPS response incomplete"));return}N=Buffer.concat(u)})}),y.on("error",D=>{x??=D}),y.once("close",()=>{E(),x?g(x):N!==null?m(N):g(Error("HTTPS response closed before completion"))}),s?.addEventListener("abort",h,{once:!0}),s?.aborted?h():y.end(i)}catch(D){E(),y?.destroy(),g(D)}})}}var tr=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function Ve(e,o){let r=M(e,o),t=r.args;if(t[0]!=="log")throw Error("DAV log report requires readonly log plan");let i=t.includes("--verbose"),n=t[i?9:8].split(":"),a=t[i?7:6],s=r.path==="/"?"":r.path.slice(1),p='<?xml version="1.0" encoding="utf-8"?><S:log-report xmlns:S="svn:" xmlns:D="DAV:"><S:start-revision>'+n[0]+"</S:start-revision><S:end-revision>"+n[1]+"</S:end-revision><S:limit>"+a+"</S:limit><S:strict-node-history/>"+(i?"<S:discover-changed-paths/>":"")+"<S:path>"+tr(s)+"</S:path><S:revprop>svn:author</S:revprop><S:revprop>svn:date</S:revprop><S:revprop>svn:log</S:revprop></S:log-report>";if(Buffer.byteLength(p)>65536)throw Error("DAV request body exceeds limit");return{method:"REPORT",body:p,pegRevision:r.pegRevision,path:r.path,requiresVerifiedBaseline:!0}}function Ie(e,{root:o,uuid:r,target:t}={}){let i=O(e),n=(f,m)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(g=>!m.includes(g)&&!(g==="#text"&&typeof f[g]=="string"&&f[g].trim()==="")))throw Error("Unsupported DAV discovery structure")};n(i,["?xml","D:multistatus"]);let a=i["D:multistatus"];if(n(a,["D:response"]),!Array.isArray(a["D:response"])||a["D:response"].length!==1)throw Error("DAV discovery requires one response");let s=a["D:response"][0];if(n(s,["D:href","D:propstat"]),R(s["D:href"],{root:o})!==t)throw Error("DAV response target mismatch");if(!Array.isArray(s["D:propstat"])||s["D:propstat"].length!==1)throw Error("DAV discovery requires one property status");let p=s["D:propstat"][0];if(n(p,["D:prop","D:status"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV discovery properties unavailable");let l=p["D:prop"];if(n(l,["S:repository-uuid","D:version-controlled-configuration"]),l["S:repository-uuid"]!==r||typeof r!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(r))throw Error("DAV UUID does not match local identity");let c=l["D:version-controlled-configuration"];return n(c,["D:href"]),{uuid:r,vcc:R(c["D:href"],{root:o}),target:t}}function Te(e,{root:o,revision:r}={}){w(r);let t=O(e),i=(f,m)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(g=>!m.includes(g)))throw Error("Invalid DAV baseline structure")};i(t,["D:multistatus"]);let n=t["D:multistatus"];if(i(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV baseline response required");let a=n["D:response"][0];i(a,["D:href","D:propstat"]);let s=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("One DAV baseline status required");let p=a["D:propstat"][0];if(i(p,["D:status","D:prop"]),p["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV baseline status unavailable");let l=p["D:prop"];if(i(l,["D:version-name","D:baseline-collection"]),l["D:version-name"]!==r)throw Error("DAV baseline revision mismatch");i(l["D:baseline-collection"],["D:href"]);let c=R(l["D:baseline-collection"]["D:href"],{root:o});if(!c.endsWith("/"))throw Error("DAV baseline collection must end in slash");return{revision:r,baseline:s,collection:c}}function Ne({request:e,assertAuthorized:o}={}){if(typeof e!="function"||typeof o!="function")throw Error("DAV chain requires transport and authorization");return async(r,{revision:t,signal:i}={})=>{w(t),L(r.root,r.scope,t);let n=r.root,a=new URL(n).origin,s=n.endsWith("/")?n:n+"/",p=async(m,g)=>{i?.throwIfAborted(),await o(r,{signal:i}),i?.throwIfAborted();let y=await e(m,{origin:a,signal:i,...g});if(i?.throwIfAborted(),await o(r,{signal:i}),i?.throwIfAborted(),!Buffer.isBuffer(y)||y.length>2097152)throw Error("Invalid DAV response bytes");return new TextDecoder("utf-8",{fatal:!0}).decode(y)},l=await p(s,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:" xmlns:S="http://subversion.tigris.org/xmlns/dav/"><D:prop><S:repository-uuid/><D:version-controlled-configuration/></D:prop></D:propfind>'}),{vcc:c}=Ie(l,{root:n,target:s,uuid:r.uuid}),f=await p(c,{method:"PROPFIND",depth:0,label:t,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:baseline-collection/><D:version-name/></D:prop></D:propfind>'});return{...Te(f,{root:n,revision:t}),uuid:r.uuid}}}function Le(e,{root:o,target:r}={}){let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV property names structure")};i(t,["D:multistatus"]);let n=t["D:multistatus"];if(i(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV property response required");let a=n["D:response"][0];if(i(a,["D:href","D:propstat"]),R(a["D:href"],{root:o})!==r)throw Error("DAV property target mismatch");if(a["D:propstat"]?.length!==1)throw Error("One DAV property status required");let s=a["D:propstat"][0];if(i(s,["D:status","D:prop"]),s["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV property names unavailable");let p=s["D:prop"];if(p!==""&&(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).length>1e3||Object.entries(p).some(([c,f])=>!/^[DSPC]:[^:]+$/.test(c)||f!=="")))throw Error("DAV property names must be empty");let l=p===""?[]:Object.keys(p);return{names:l,special:l.includes("P:special")}}var K=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""");function ke({request:e}={}){if(typeof e!="function")throw Error("Bounded DAV request required");return async(o,{identity:r,assertAuthorized:t,signal:i}={})=>{let n=M(o,r),a=n.args[0],s=Ne({request:e,assertAuthorized:t}),p=await s(r,{revision:n.pegRevision,signal:i}),l=async(y,S)=>{i?.throwIfAborted(),await t(),i?.throwIfAborted();let x=await e(y,{origin:new URL(r.root).origin,...S,signal:i});if(i?.throwIfAborted(),await t(),i?.throwIfAborted(),!Buffer.isBuffer(x)||x.length>2097152)throw Error("Invalid DAV transport bytes");return x};if(a==="log"){let y=Ve(o,r);return l(p.collection,{method:"REPORT",body:y.body})}let c=n.path.slice(1).split("/").map(y=>encodeURIComponent(y).replace(/[!'()*]/g,S=>"%"+S.charCodeAt(0).toString(16).toUpperCase())).join("/"),f=R(p.collection+c,{root:r.root});if(a==="info"){let y=await l(f,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:resourcetype/></D:prop></D:propfind>'}),S=he(new TextDecoder("utf-8",{fatal:!0}).decode(y),{root:r.root,target:f}),x=n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@"));return'<info><entry kind="'+S+'" revision="'+n.pegRevision+'"><url>'+K(x)+"</url><repository><root>"+K(r.root)+"</root><uuid>"+K(r.uuid)+"</uuid></repository></entry></info>"}let m=await l(f,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:propname/></D:propfind>'}),g=Le(new TextDecoder("utf-8",{fatal:!0}).decode(m),{root:r.root,target:f});if(a==="proplist")return'<properties><target path="'+K(n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@")))+'">'+(g.special?'<property name="svn:special"/>':"")+"</target></properties>";if(g.special)throw Error("SVN special content forbidden");return l(f,{method:"GET"})}}function Jt(e,o){return xe(e,o,{transportFormat:"dav",transport:ke({request:Ae()})})}export{Jt as createSvnHttpsHost};
|
package/index.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import {readFileSync} from 'node:fs';
|
|
|
3
3
|
import {registerFrontendAssets} from './frontend-assets.mjs';
|
|
4
4
|
const runningFrontend=JSON.parse(readFileSync(new URL('./dist/frontend.json',import.meta.url),'utf8'));
|
|
5
5
|
import * as adapter from './vcs.mjs';
|
|
6
|
+
import * as branchAdapter from './branch-switch.mjs';
|
|
7
|
+
import {createBranchSwitchCoordinator,acquireRepositoryAccess} from './branch-switch-host.mjs';
|
|
6
8
|
import {withOperationBudget} from './src/operation-budget.mjs';
|
|
7
9
|
import {runtimeInfo,checkUpdate} from './dist/updates.mjs';
|
|
8
10
|
import {createSvnHttpsHost} from './dist/svn-host.mjs';
|
|
@@ -34,8 +36,11 @@ const REQUEST_FIELDS = {
|
|
|
34
36
|
'vcs/repositories': ['sessionId', 'subdirectory'],
|
|
35
37
|
'vcs/svn-identity': ['sessionId', 'repositoryId'],
|
|
36
38
|
'vcs/references': ['sessionId', 'repositoryId'],
|
|
39
|
+
'vcs/branches': ['sessionId', 'repositoryId'],
|
|
40
|
+
'vcs/branch-switch-prepare': ['sessionId', 'repositoryId', 'branch'],
|
|
41
|
+
'vcs/branch-switch-confirm': ['sessionId', 'repositoryId', 'token'],
|
|
37
42
|
'vcs/stash-list': ['sessionId', 'repositoryId'],
|
|
38
|
-
'vcs/grep': ['sessionId', 'repositoryId', 'commit', 'query'],
|
|
43
|
+
'vcs/grep': ['sessionId', 'repositoryId', 'commit', 'query', 'cursor', 'limit'],
|
|
39
44
|
'vcs/status': ['sessionId', 'repositoryId', 'mode'],
|
|
40
45
|
'vcs/compare': ['sessionId', 'repositoryId', 'mode', 'id', 'large'],
|
|
41
46
|
'vcs/conflict-stages': ['sessionId', 'repositoryId', 'mode', 'id'],
|
|
@@ -50,7 +55,7 @@ const REQUEST_FIELDS = {
|
|
|
50
55
|
'vcs/tree': ['sessionId', 'repositoryId', 'commit'],
|
|
51
56
|
'vcs/tree-page': ['sessionId', 'repositoryId', 'commit', 'path', 'offset', 'limit', 'query'],
|
|
52
57
|
'vcs/tree-file': ['sessionId', 'repositoryId', 'commit', 'path'],
|
|
53
|
-
'vcs/tree-segment': ['sessionId', 'repositoryId', 'commit', 'path', 'offset'],
|
|
58
|
+
'vcs/tree-segment': ['sessionId', 'repositoryId', 'commit', 'path', 'offset', 'line'],
|
|
54
59
|
'vcs/tree-history': ['sessionId', 'repositoryId', 'commit', 'path', 'offset', 'limit', 'follow', 'since', 'until'],
|
|
55
60
|
'vcs/commit-image': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'side'],
|
|
56
61
|
'vcs/workspace-image': ['sessionId', 'repositoryId', 'mode', 'id', 'side'],
|
|
@@ -130,6 +135,8 @@ function validateContentSearch(endpoint, payload) {
|
|
|
130
135
|
if (endpoint !== 'vcs/grep') return;
|
|
131
136
|
if (typeof payload.query !== 'string' || !payload.query || payload.query.length > 1024 || /[\0\r\n]/.test(payload.query)) throw invalid('Invalid content search query.');
|
|
132
137
|
if (payload.commit !== undefined && (typeof payload.commit !== 'string' || !isObjectId(payload.commit))) throw invalid('Invalid content search commit.');
|
|
138
|
+
if (payload.cursor !== undefined && (typeof payload.cursor !== 'string' || !payload.cursor || payload.cursor.length > 4096 || !payload.commit)) throw invalid('Invalid content search cursor.');
|
|
139
|
+
if (payload.limit !== undefined && (!Number.isInteger(payload.limit) || payload.limit < 1 || payload.limit > 100)) throw invalid('Invalid content search page size.');
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
function validateRenameFollow(endpoint, payload) {
|
|
@@ -164,6 +171,8 @@ function validateModeAndComparisonId(endpoint, payload) {
|
|
|
164
171
|
/** The validation order below is part of the protocol: the first failing group wins. */
|
|
165
172
|
function validateRepositoryPayload(endpoint, payload) {
|
|
166
173
|
if (typeof payload.repositoryId !== 'string' || !payload.repositoryId || payload.repositoryId.length > 1024) throw invalid('A discovered repositoryId is required.');
|
|
174
|
+
if(endpoint==='vcs/branch-switch-prepare'&&(typeof payload.branch!=='string'||!payload.branch||payload.branch.length>1024||/[\0\r\n]/.test(payload.branch)))throw invalid('A local branch name is required.');
|
|
175
|
+
if(endpoint==='vcs/branch-switch-confirm'&&(typeof payload.token!=='string'||! /^[a-f0-9]{64}$/.test(payload.token)))throw invalid('A valid branch confirmation token is required.');
|
|
167
176
|
validateHistoricalPath(endpoint, payload.path);
|
|
168
177
|
validateImageSide(endpoint, payload.side);
|
|
169
178
|
validateRevisionSelection(endpoint, payload);
|
|
@@ -171,6 +180,7 @@ function validateRepositoryPayload(endpoint, payload) {
|
|
|
171
180
|
validateCommitSelection(endpoint, payload);
|
|
172
181
|
validateLargeComparison(endpoint, payload);
|
|
173
182
|
if (endpoint === 'vcs/tree-segment' && payload.offset !== undefined && (!Number.isInteger(payload.offset) || payload.offset < 0 || payload.offset > 16777216)) throw invalid('Invalid segment offset.');
|
|
183
|
+
if (endpoint === 'vcs/tree-segment' && payload.line !== undefined && (!Number.isSafeInteger(payload.line) || payload.line < 1 || payload.line > 16777217 || payload.offset !== undefined)) throw invalid('Invalid segment target line.');
|
|
174
184
|
validateBlameWindow(endpoint, payload);
|
|
175
185
|
validateContentSearch(endpoint, payload);
|
|
176
186
|
validateRenameFollow(endpoint, payload);
|
|
@@ -208,7 +218,7 @@ async function readRepository(api, endpoint, repository, payload, {cwd, mode, si
|
|
|
208
218
|
...optionalField('since', payload.since), ...optionalField('until', payload.until), signal,
|
|
209
219
|
});
|
|
210
220
|
case 'vcs/tree-segment':
|
|
211
|
-
return api.getHistoricalSegment({...repository}, {commit: payload.commit, path: payload.path, offset
|
|
221
|
+
return api.getHistoricalSegment({...repository}, {commit: payload.commit, path: payload.path, ...optionalField('offset', payload.offset), ...optionalField('line', payload.line), signal});
|
|
212
222
|
case 'vcs/workspace-image':
|
|
213
223
|
return api.getWorkspaceImage({...repository}, {mode, id: payload.id, side: payload.side ?? 'right', signal});
|
|
214
224
|
case 'vcs/revision-image':
|
|
@@ -218,7 +228,7 @@ async function readRepository(api, endpoint, repository, payload, {cwd, mode, si
|
|
|
218
228
|
case 'vcs/stash-list':
|
|
219
229
|
return api.listStashes({...repository}, {signal});
|
|
220
230
|
case 'vcs/grep':
|
|
221
|
-
return api.searchRepositoryContent({...repository}, {commit: payload.commit, query: payload.query, signal});
|
|
231
|
+
return api.searchRepositoryContent({...repository}, {commit: payload.commit, query: payload.query, ...optionalField('cursor', payload.cursor), ...optionalField('limit', payload.limit), signal});
|
|
222
232
|
case 'vcs/commit-image':
|
|
223
233
|
return api.getCommitImage({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, side: payload.side ?? 'right', signal});
|
|
224
234
|
case 'vcs/tree-file':
|
|
@@ -255,11 +265,13 @@ async function readRepository(api, endpoint, repository, payload, {cwd, mode, si
|
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
/** Limit active adapter operations and retain only bounded, short-lived discovery grants. */
|
|
258
|
-
export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.now, ttlMs = 5 * 60_000, maxSessions = 32, maxRepositories = 512, operationTimeoutMs = 15000, bindSvnIdentityResolver } = {}) {
|
|
268
|
+
export function createHandler(ctx, api = {...adapter,...branchAdapter}, maxActive = 4, { now = Date.now, ttlMs = 5 * 60_000, maxSessions = 32, maxRepositories = 512, operationTimeoutMs = 15000, bindSvnIdentityResolver } = {}) {
|
|
259
269
|
let active = 0, activeImages = 0, activeSegments = 0;
|
|
260
270
|
const sessions = new Map();
|
|
271
|
+
const branchSwitch=createBranchSwitchCoordinator({api,now});
|
|
261
272
|
|
|
262
273
|
const clearGrantCache = entry => {
|
|
274
|
+
branchSwitch.invalidate(entry);
|
|
263
275
|
for (const repository of entry?.repositories.values() ?? []) api.clearHistoricalCache?.(repository.root);
|
|
264
276
|
};
|
|
265
277
|
const revokeSession = id => { clearGrantCache(sessions.get(id)); sessions.delete(id); };
|
|
@@ -401,12 +413,23 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
401
413
|
// Both adapter operations revalidate the canonical root; comparison also validates
|
|
402
414
|
// the change ID against fresh status, so do not duplicate a full status scan here.
|
|
403
415
|
if (GIT_ONLY_ENDPOINTS.has(endpoint) && repository.type !== 'git') throw invalid('History currently supports Git only.');
|
|
416
|
+
const authorize=()=>assertCurrentGrant(payload.sessionId,cwd,entry,payload.repositoryId,repository,signal);
|
|
417
|
+
if(endpoint==='vcs/branch-switch-confirm'){
|
|
418
|
+
try{return {ok:true,value:await branchSwitch.confirm(payload,{entry,repository,cwd,signal,assertAuthorized:authorize})};}
|
|
419
|
+
finally{api.clearHistoricalCache?.(repository.root);}
|
|
420
|
+
}
|
|
421
|
+
const releaseRepository=acquireRepositoryAccess(repository.root);
|
|
422
|
+
try{
|
|
423
|
+
if(endpoint==='vcs/branches'){const value=await api.listLocalBranches({...repository},{signal});await authorize();return {ok:true,value};}
|
|
424
|
+
if(endpoint==='vcs/branch-switch-prepare')return {ok:true,value:await branchSwitch.prepare(payload,{entry,repository,cwd,signal,assertAuthorized:authorize})};
|
|
404
425
|
const admission = admitHeavyOperation(endpoint, payload);
|
|
405
426
|
imageSlot = admission.imageSlot;
|
|
406
427
|
segmentSlot = admission.segmentSlot;
|
|
407
428
|
if (admission.busy) return admission.busy;
|
|
408
429
|
return { ok: true, value: await readGrantedRepository(endpoint, repository, payload, cwd, mode, entry, signal) };
|
|
430
|
+
}finally{releaseRepository();}
|
|
409
431
|
} catch (error) {
|
|
432
|
+
if(error?.code==='vcs/branch-outcome-unknown')return failure(error.code,error.message);
|
|
410
433
|
if (signal?.aborted || error?.name === 'AbortError') return failure('vcs/cancelled', 'The request was cancelled.');
|
|
411
434
|
const code = error?.code === 'TIMEOUT' ? 'vcs/timeout' : typeof error?.code === 'string' && error.code.startsWith('vcs/') ? error.code : 'vcs/operation-failed';
|
|
412
435
|
return failure(code, error instanceof Error ? error.message : String(error));
|
|
@@ -416,14 +439,18 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
416
439
|
if (segmentSlot) activeSegments--;
|
|
417
440
|
}
|
|
418
441
|
};
|
|
419
|
-
|
|
442
|
+
const handle=async (endpoint, payload, signal) => {
|
|
420
443
|
try {
|
|
421
444
|
return await withOperationBudget({signal, timeoutMs: operationTimeoutMs}, () => execute(endpoint, payload, signal));
|
|
422
445
|
} catch (error) {
|
|
446
|
+
if(error?.code==='vcs/branch-outcome-unknown')return failure(error.code,error.message);
|
|
447
|
+
if(endpoint==='vcs/branch-switch-confirm')return failure('vcs/branch-outcome-unknown','Confirmation did not complete within its request lifetime. Refresh and inspect the current branch before preparing again.');
|
|
423
448
|
if (signal?.aborted || error?.name === 'AbortError') return failure('vcs/cancelled', 'The request was cancelled.');
|
|
424
449
|
return failure(error?.code === 'TIMEOUT' ? 'vcs/timeout' : 'vcs/operation-failed', error.message);
|
|
425
450
|
}
|
|
426
451
|
};
|
|
452
|
+
handle.dispose=()=>{branchSwitch.clear();for(const id of sessions.keys())revokeSession(id);};
|
|
453
|
+
return handle;
|
|
427
454
|
}
|
|
428
455
|
|
|
429
456
|
/** Content type is fixed per build artifact; URL input never chooses it. */
|
|
@@ -473,7 +500,7 @@ export function registerDirectAssets(ctx, load = filename => readFile(new URL('.
|
|
|
473
500
|
export function apply(ctx) {
|
|
474
501
|
// Pass the plugin owner explicitly: the nested rpc getter can retain the
|
|
475
502
|
// connection service context, which does not inject webServer.
|
|
476
|
-
const host=createSvnHttpsHost(ctx,adapter);
|
|
503
|
+
const host=createSvnHttpsHost(ctx,{...adapter,...branchAdapter});
|
|
477
504
|
ctx.effect(()=>()=>{ host.dispose(); adapter.clearHistoricalCache?.(); });
|
|
478
505
|
ctx.connection.register(ctx, RPC_CHANNEL, host.handle);
|
|
479
506
|
registerAssets(ctx);
|
package/locale/en.json
CHANGED
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-vcs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
6
6
|
"type": "module",
|
|
7
|
-
"description": "
|
|
7
|
+
"description": "Git and SVN change preview with confirmed safe local Git branch switching for DSH",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.mjs",
|
|
10
10
|
"./client": "./dist/client.js",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"index.mjs",
|
|
16
16
|
"frontend-assets.mjs",
|
|
17
17
|
"vcs.mjs",
|
|
18
|
+
"branch-switch.mjs",
|
|
19
|
+
"branch-switch-host.mjs",
|
|
18
20
|
"git-history.mjs",
|
|
19
21
|
"conflict-stages.mjs",
|
|
20
22
|
"image-preview.mjs",
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
"src/commit-signature.mjs",
|
|
30
32
|
"src/operation-budget.mjs",
|
|
31
33
|
"src/historical-access.mjs",
|
|
34
|
+
"src/content-search.mjs",
|
|
32
35
|
"src/preview-capabilities.mjs",
|
|
33
36
|
"dist",
|
|
34
37
|
"locale",
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {createHash,createHmac,randomBytes,timingSafeEqual} from 'node:crypto';
|
|
2
|
+
import {parseTreeEntries} from './historical-access.mjs';
|
|
3
|
+
import {currentOperationBudget} from './operation-budget.mjs';
|
|
4
|
+
|
|
5
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
6
|
+
const secret = randomBytes(32);
|
|
7
|
+
const invalid = () => Error('Invalid content search cursor');
|
|
8
|
+
const binding = (root,snapshot,query) => createHash('sha256').update(JSON.stringify([root,snapshot,query])).digest('hex');
|
|
9
|
+
const sign = body => createHmac('sha256',secret).update(body).digest('base64url');
|
|
10
|
+
function encode(state) { const body=Buffer.from(JSON.stringify(state)).toString('base64url');return body+'.'+sign(body); }
|
|
11
|
+
function decode(cursor,root,snapshot,query) {
|
|
12
|
+
if(typeof cursor!=='string'||cursor.length>4096)throw invalid();
|
|
13
|
+
const [body,mac,...extra]=cursor.split('.');
|
|
14
|
+
if(!body||!mac||extra.length||!/^[A-Za-z0-9_-]+$/.test(body)||!/^[A-Za-z0-9_-]{43}$/.test(mac)||!timingSafeEqual(Buffer.from(mac),Buffer.from(sign(body))))throw invalid();
|
|
15
|
+
let state;try{state=JSON.parse(Buffer.from(body,'base64url').toString());}catch{throw invalid();}
|
|
16
|
+
if(state.binding!==binding(root,snapshot,query)||!Number.isSafeInteger(state.index)||state.index<0||!Number.isSafeInteger(state.offset)||state.offset<0||state.offset>MAX_BYTES||!Number.isSafeInteger(state.line)||state.line<1||typeof state.skipped!=='boolean')throw invalid();
|
|
17
|
+
return state;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Stable object/tree positions, not an offset into a capped grep result set. */
|
|
21
|
+
export async function searchContentPage(run,root,{snapshot,query,cursor,limit=100,signal}) {
|
|
22
|
+
if(!Number.isInteger(limit)||limit<1||limit>100)throw Error('Invalid content search page size');
|
|
23
|
+
const check=()=>{signal?.throwIfAborted();currentOperationBudget()?.throwIfExpired();};
|
|
24
|
+
check();
|
|
25
|
+
const state=cursor===undefined?{binding:binding(root,snapshot,query),index:0,offset:0,line:1,skipped:false}:decode(cursor,root,snapshot,query);
|
|
26
|
+
const listing=await run(root,['ls-tree','-r','-z','--full-tree',snapshot],MAX_BYTES,{signal});
|
|
27
|
+
const entries=parseTreeEntries(listing).filter(entry=>entry.type==='blob'&&['100644','100755'].includes(entry.mode));
|
|
28
|
+
if(state.index>entries.length)throw invalid();
|
|
29
|
+
const matches=[],needle=Buffer.from(query),warnings=[];
|
|
30
|
+
let scannedBytes=0,responseBytes=0,files=0,reason=null;
|
|
31
|
+
while(state.index<entries.length) {
|
|
32
|
+
check();
|
|
33
|
+
if(matches.length>=limit){reason='page-limit';break;}
|
|
34
|
+
if(files>=64||scannedBytes>=MAX_BYTES){reason='scan-budget';break;}
|
|
35
|
+
const entry=entries[state.index];
|
|
36
|
+
const rawSize=(await run(root,['cat-file','-s',entry.oid],2097152,{signal})).toString().trim();
|
|
37
|
+
if(!/^\d+$/.test(rawSize)||!Number.isSafeInteger(Number(rawSize)))throw Error('Invalid search object size');
|
|
38
|
+
const size=Number(rawSize);
|
|
39
|
+
if(size>MAX_BYTES){state.skipped=true;state.index++;state.offset=0;state.line=1;files++;continue;}
|
|
40
|
+
if(files&&scannedBytes+size>MAX_BYTES){reason='scan-budget';break;}
|
|
41
|
+
const body=await run(root,['cat-file','blob',entry.oid],Math.max(1,size),{signal});
|
|
42
|
+
scannedBytes+=body.length;files++;
|
|
43
|
+
if(body.length!==size||state.offset>size)throw Error('Search object size mismatch');
|
|
44
|
+
// Match git grep -I: binary files are outside this text-search domain.
|
|
45
|
+
if(body.includes(0)){state.index++;state.offset=0;state.line=1;continue;}
|
|
46
|
+
let pos=state.offset,line=state.line;
|
|
47
|
+
while(pos<body.length) {
|
|
48
|
+
check();
|
|
49
|
+
// Single pass: CR-only input must not rescan the remaining blob for LF per line.
|
|
50
|
+
let end=pos;
|
|
51
|
+
while(end<body.length&&body[end]!==10&&body[end]!==13){
|
|
52
|
+
if((end & 65535)===0)check();
|
|
53
|
+
end++;
|
|
54
|
+
}
|
|
55
|
+
const hit=body.subarray(pos,end).indexOf(needle);
|
|
56
|
+
if(hit>=0) {
|
|
57
|
+
let textEnd=Math.min(end,pos+4096);
|
|
58
|
+
while(textEnd<end&&(body[textEnd]&192)===128)textEnd--;
|
|
59
|
+
const match={path:entry.path,line,text:body.subarray(pos,textEnd).toString('utf8'),...(textEnd<end?{textTruncated:true}:{})};
|
|
60
|
+
const matchBytes=Buffer.byteLength(JSON.stringify(match));
|
|
61
|
+
if(responseBytes+matchBytes>1024*1024){reason='page-limit';break;}
|
|
62
|
+
matches.push(match);responseBytes+=matchBytes;
|
|
63
|
+
}
|
|
64
|
+
pos=end<body.length?end+(body[end]===13&&body[end+1]===10?2:1):end;line++;
|
|
65
|
+
if(matches.length>=limit)break;
|
|
66
|
+
}
|
|
67
|
+
if(pos>=body.length){state.index++;state.offset=0;state.line=1;}
|
|
68
|
+
else {state.offset=pos;state.line=line;reason='page-limit';break;}
|
|
69
|
+
}
|
|
70
|
+
check();
|
|
71
|
+
const more=state.index<entries.length;
|
|
72
|
+
if(state.skipped)warnings.push('Files exceeding the 16 MiB text-search limit were skipped.');
|
|
73
|
+
return {snapshot,query,matches,nextCursor:more?encode(state):null,complete:!more&&!state.skipped,truncated:more||state.skipped,reason:reason??(state.skipped?'unsupported-content':null),warnings};
|
|
74
|
+
}
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import {isObjectId} from '../git-history.mjs';
|
|
2
|
+
import {currentOperationBudget} from './operation-budget.mjs';
|
|
2
3
|
|
|
3
4
|
export const HISTORICAL_CACHE_LIMITS = Object.freeze({maxBytes: 32 * 1024 * 1024, maxEntries: 8, ttlMs: 60000});
|
|
4
5
|
const MAX_BLOB_BYTES = 16 * 1024 * 1024;
|
|
5
6
|
const MAX_LIST_BYTES = 16 * 1024 * 1024;
|
|
6
7
|
const MAX_DIRECTORY_ENTRIES = 100000;
|
|
7
8
|
const CHUNK_BYTES = 64 * 1024;
|
|
9
|
+
const isLineBreak = (buffer,pos) => buffer[pos] === 10 || (buffer[pos] === 13 && buffer[pos + 1] !== 10);
|
|
10
|
+
// At most 257 checkpoints for a 16 MiB blob, independent of its line count.
|
|
11
|
+
function buildLineIndex(buffer,signal) {
|
|
12
|
+
const index=[];let line=1,lineStart=0;
|
|
13
|
+
for(let offset=0;offset<=buffer.length;offset++) {
|
|
14
|
+
if(offset%CHUNK_BYTES===0){signal?.throwIfAborted();currentOperationBudget()?.throwIfExpired();index.push({offset,line,lineStart});}
|
|
15
|
+
if(isLineBreak(buffer,offset)){line++;lineStart=offset+1;}
|
|
16
|
+
}
|
|
17
|
+
return {index,totalLines:line};
|
|
18
|
+
}
|
|
19
|
+
function locateOffset(buffer,index,offset) {
|
|
20
|
+
const point=index[Math.floor(offset/CHUNK_BYTES)];let line=point.line,lineStart=point.lineStart;
|
|
21
|
+
for(let pos=point.offset;pos<offset;pos++)if(isLineBreak(buffer,pos)){line++;lineStart=pos+1;}
|
|
22
|
+
return {line,lineStart};
|
|
23
|
+
}
|
|
24
|
+
function locateLine(buffer,index,line) {
|
|
25
|
+
let point=index[0];for(const candidate of index){if(candidate.line>line)break;point=candidate;}
|
|
26
|
+
if(point.line===line)return point.lineStart;
|
|
27
|
+
let current=point.line;
|
|
28
|
+
for(let pos=point.offset;pos<buffer.length;pos++)if(isLineBreak(buffer,pos)&&++current===line)return pos+1;
|
|
29
|
+
return buffer.length;
|
|
30
|
+
}
|
|
8
31
|
|
|
9
32
|
export function validateHistoricalPath(file, allowRoot = false) {
|
|
10
33
|
if (typeof file !== 'string' || file.length > 32768 || (!file && !allowRoot) || file.includes('\0') || file.startsWith('/') || file.split('/').some(part => (!part || part === '.' || part === '..') && !(allowRoot && file === ''))) {
|
|
@@ -87,14 +110,17 @@ export function createHistoricalAccess({run, now = Date.now, maxBytes = HISTORIC
|
|
|
87
110
|
filtered.sort((a,b) => Number(b.type === 'tree') - Number(a.type === 'tree') || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
88
111
|
return {commit,path:directory,query,entries:filtered.slice(offset,offset + limit),nextOffset:offset + limit < filtered.length ? offset + limit : null};
|
|
89
112
|
};
|
|
90
|
-
const segment = async (root, {commit,path:file,offset
|
|
113
|
+
const segment = async (root, {commit,path:file,offset:requestedOffset,line:targetLine,signal,expectedEpoch = epoch} = {}) => {
|
|
91
114
|
const generation = expectedEpoch; // Includes caller repository validation and all member lookups.
|
|
115
|
+
if(targetLine !== undefined && (!Number.isSafeInteger(targetLine) || targetLine < 1 || targetLine > MAX_BLOB_BYTES + 1 || requestedOffset !== undefined)) throw Error('Invalid segment target line');
|
|
116
|
+
let offset = requestedOffset ?? 0;
|
|
92
117
|
if (!Number.isInteger(offset) || offset < 0 || offset > MAX_BLOB_BYTES) throw Error('Invalid segment offset');
|
|
93
118
|
const entry = await lookup(root, {commit,path:file,signal,missingMessage:'Segment requires a selected committed regular file'});
|
|
94
119
|
if (entry.type !== 'blob' || !['100644','100755'].includes(entry.mode)) throw Error('Segment requires a selected committed regular file');
|
|
95
120
|
purge();
|
|
96
121
|
const key = JSON.stringify([root,commit,entry.oid]);
|
|
97
|
-
let
|
|
122
|
+
let cached = generation === epoch ? cache.get(key) : undefined;
|
|
123
|
+
let buffer = cached?.buffer, lineIndex = cached?.lineIndex;
|
|
98
124
|
if (buffer) cacheHits++;
|
|
99
125
|
if (!buffer) {
|
|
100
126
|
const rawSize = (await run(root, ['cat-file','-s',entry.oid], 2097152, {signal})).toString('utf8').trim();
|
|
@@ -116,24 +142,32 @@ export function createHistoricalAccess({run, now = Date.now, maxBytes = HISTORIC
|
|
|
116
142
|
if (buffer.includes(0) || buffer.some(byte => byte < 32 && ![9,10,12,13].includes(byte))) throw Error('Segments support UTF-8 text only');
|
|
117
143
|
new TextDecoder('utf-8',{fatal:true}).decode(buffer);
|
|
118
144
|
decodeCount++;
|
|
145
|
+
lineIndex = buildLineIndex(buffer,signal);
|
|
119
146
|
if (generation === epoch) {
|
|
120
147
|
remove(key);
|
|
121
148
|
const timer = setTimeout(() => remove(key), ttlMs);
|
|
122
149
|
timer.unref?.();
|
|
123
|
-
cache.set(key, {root,buffer,expiresAt:now() + ttlMs,timer});
|
|
150
|
+
cache.set(key, {root,buffer,lineIndex,expiresAt:now() + ttlMs,timer});
|
|
124
151
|
bytes += buffer.length;
|
|
125
152
|
}
|
|
126
153
|
} finally {reservedBytes -= size; reservedEntries--;}
|
|
127
154
|
}
|
|
128
155
|
signal?.throwIfAborted();
|
|
129
156
|
const size = buffer.length;
|
|
157
|
+
const targetFound = targetLine === undefined || targetLine <= lineIndex.totalLines;
|
|
158
|
+
if(targetLine !== undefined) offset = targetFound ? locateLine(buffer,lineIndex.index,targetLine) : size;
|
|
130
159
|
if (offset > size) throw Error('Segment offset exceeds file size');
|
|
160
|
+
const start = locateOffset(buffer,lineIndex.index,offset);
|
|
131
161
|
if (offset < size && (buffer[offset] & 192) === 128) throw Error('Segment offset splits UTF-8 character');
|
|
132
162
|
let end = Math.min(size, offset + CHUNK_BYTES);
|
|
133
163
|
while (end < size && (buffer[end] & 192) === 128) end--;
|
|
164
|
+
if(end < size && buffer[end - 1] === 13 && buffer[end] === 10) end--;
|
|
134
165
|
const text = new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(buffer.subarray(offset,end));
|
|
135
166
|
signal?.throwIfAborted();
|
|
136
|
-
|
|
167
|
+
const endLine = locateOffset(buffer,lineIndex.index,end).line;
|
|
168
|
+
const endsMidLine = end < size && end > offset && !isLineBreak(buffer,end - 1);
|
|
169
|
+
const lineTruncated = endsMidLine;
|
|
170
|
+
return {commit,path:file,oid:entry.oid,encoding:'UTF-8',offset,endOffset:end,totalBytes:size,nextOffset:end < size ? end : null,text,startLine:start.line,endLine,startsMidLine:offset > start.lineStart,targetFound,lineTruncated,endsMidLine,...(!targetFound?{notice:'Requested line exceeds file line count.'}:{})};
|
|
137
171
|
};
|
|
138
172
|
return {lookup,treePage,segment,clear,captureEpoch:() => epoch,stats:() => {purge();return {bytes,entries:cache.size,reservedBytes,reservedEntries,blobReads,blobReadBytes,decodeCount,cacheHits};}};
|
|
139
173
|
}
|