agi.7z.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. package agi
  2. /*
  3. AGI 7-Zip Library Extension
  4. Adds 7z archive support to the ziplib namespace.
  5. Call inject7zLibFunctions from injectZipFileLibFunctions (after the main
  6. vm.Run that creates the ziplib object) so the bindings can extend it.
  7. Depends on github.com/bodgit/sevenzip (BSD-3-Clause).
  8. */
  9. import (
  10. "encoding/json"
  11. "errors"
  12. "os"
  13. "path/filepath"
  14. "strings"
  15. "github.com/bodgit/sevenzip"
  16. "github.com/robertkrimen/otto"
  17. "imuslab.com/arozos/mod/agi/static"
  18. "imuslab.com/arozos/mod/filesystem"
  19. "imuslab.com/arozos/mod/filesystem/arozfs"
  20. )
  21. // inject7zLibFunctions registers all 7z functions into the ziplib JS namespace.
  22. // Must be called after the main vm.Run block that creates `var ziplib = {}`.
  23. func (g *Gateway) inject7zLibFunctions(payload *static.AgiLibInjectionPayload) {
  24. vm := payload.VM
  25. u := payload.User
  26. scriptFsh := payload.ScriptFsh
  27. // list7zFileDir(srcVpath, dirPath) --> string[]
  28. // List the immediate children of a directory inside a 7z archive.
  29. vm.Set("_ziplib_list7zFileDir", func(call otto.FunctionCall) otto.Value {
  30. zipVpath, err := call.Argument(0).ToString()
  31. if err != nil {
  32. g.RaiseError(err)
  33. return otto.NullValue()
  34. }
  35. dirPath, err := call.Argument(1).ToString()
  36. if err != nil {
  37. dirPath = ""
  38. }
  39. zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
  40. if !u.CanRead(zipVpath) {
  41. panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
  42. }
  43. _, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
  44. if err != nil {
  45. g.RaiseError(err)
  46. return otto.NullValue()
  47. }
  48. filelist, err := szListDir(zipRpath, dirPath)
  49. if err != nil {
  50. g.RaiseError(err)
  51. return otto.NullValue()
  52. }
  53. reply, _ := vm.ToValue(filelist)
  54. return reply
  55. })
  56. // list7zFileContents(srcVpath) --> JSON string (tree)
  57. // Return the full JSON tree of all entries in a 7z archive.
  58. vm.Set("_ziplib_list7zFileContents", func(call otto.FunctionCall) otto.Value {
  59. zipVpath, err := call.Argument(0).ToString()
  60. if err != nil {
  61. g.RaiseError(err)
  62. return otto.NullValue()
  63. }
  64. zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
  65. if !u.CanRead(zipVpath) {
  66. panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
  67. }
  68. _, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
  69. if err != nil {
  70. g.RaiseError(err)
  71. return otto.NullValue()
  72. }
  73. jsonStr, err := sz7zListContentsJSON(zipRpath)
  74. if err != nil {
  75. g.RaiseError(err)
  76. return otto.NullValue()
  77. }
  78. reply, _ := vm.ToValue(jsonStr)
  79. return reply
  80. })
  81. // getFileFrom7z(srcVpath, filePathIn7z) --> vpath of extracted file in tmp:/
  82. // Extract a single file from a 7z archive to a temporary location.
  83. vm.Set("_ziplib_getFileFrom7z", func(call otto.FunctionCall) otto.Value {
  84. zipVpath, err := call.Argument(0).ToString()
  85. if err != nil {
  86. g.RaiseError(err)
  87. return otto.NullValue()
  88. }
  89. fileIn7z, err := call.Argument(1).ToString()
  90. if err != nil {
  91. g.RaiseError(err)
  92. return otto.NullValue()
  93. }
  94. zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
  95. if !u.CanRead(zipVpath) {
  96. panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
  97. }
  98. _, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
  99. if err != nil {
  100. g.RaiseError(err)
  101. return otto.NullValue()
  102. }
  103. r, err := sevenzip.OpenReader(zipRpath)
  104. if err != nil {
  105. g.RaiseError(err)
  106. return otto.NullValue()
  107. }
  108. defer r.Close()
  109. var target *sevenzip.File
  110. for _, f := range r.File {
  111. if filepath.ToSlash(f.Name) == filepath.ToSlash(fileIn7z) {
  112. target = f
  113. break
  114. }
  115. }
  116. if target == nil {
  117. g.RaiseError(errors.New("File not found in archive: " + fileIn7z))
  118. return otto.NullValue()
  119. }
  120. tmpFsh, err := u.GetFileSystemHandlerFromVirtualPath("tmp:/")
  121. if err != nil {
  122. g.RaiseError(err)
  123. return otto.NullValue()
  124. }
  125. tmpFilename := arozfs.Base(fileIn7z)
  126. tmpVpath := "tmp:/" + tmpFilename
  127. tmpRpath, _ := tmpFsh.FileSystemAbstraction.VirtualPathToRealPath(tmpVpath, u.Username)
  128. rc, err := target.Open()
  129. if err != nil {
  130. g.RaiseError(err)
  131. return otto.NullValue()
  132. }
  133. defer rc.Close()
  134. if err = tmpFsh.FileSystemAbstraction.WriteStream(tmpRpath, rc, 0755); err != nil {
  135. g.RaiseError(err)
  136. return otto.NullValue()
  137. }
  138. u.SetOwnerOfFile(tmpFsh, tmpVpath)
  139. reply, _ := vm.ToValue(tmpVpath)
  140. return reply
  141. })
  142. // extract7zFile(srcVpath, destVpath) --> bool
  143. // Extract all files from a 7z archive to destVpath.
  144. vm.Set("_ziplib_extract7zFile", func(call otto.FunctionCall) otto.Value {
  145. srcVpath, err := call.Argument(0).ToString()
  146. if err != nil {
  147. g.RaiseError(err)
  148. return otto.FalseValue()
  149. }
  150. destVpath, err := call.Argument(1).ToString()
  151. if err != nil {
  152. g.RaiseError(err)
  153. return otto.FalseValue()
  154. }
  155. srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
  156. destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
  157. if !u.CanRead(srcVpath) {
  158. panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
  159. }
  160. if !u.CanWrite(destVpath) {
  161. panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
  162. }
  163. _, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
  164. if err != nil {
  165. g.RaiseError(err)
  166. return otto.FalseValue()
  167. }
  168. destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
  169. if err != nil {
  170. g.RaiseError(err)
  171. return otto.FalseValue()
  172. }
  173. if err = sz7zExtractAll(srcRpath, destFsh, destRpath); err != nil {
  174. g.RaiseError(err)
  175. return otto.FalseValue()
  176. }
  177. destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
  178. if err == nil && !info.IsDir() {
  179. vp, e := static.RealpathToVirtualpath(destFsh, path, u)
  180. if e == nil {
  181. u.SetOwnerOfFile(destFsh, vp)
  182. }
  183. }
  184. return nil
  185. })
  186. reply, _ := vm.ToValue(true)
  187. return reply
  188. })
  189. // extractPartial7z(srcVpath, filePathsIn7z, destVpath) --> bool
  190. // Extract selected files/folders from a 7z archive; strips parent prefix for
  191. // folder selections, extracts flat for file selections (same semantics as
  192. // extractPartialZip).
  193. vm.Set("_ziplib_extractPartial7z", func(call otto.FunctionCall) otto.Value {
  194. zipVpath, err := call.Argument(0).ToString()
  195. if err != nil {
  196. g.RaiseError(err)
  197. return otto.FalseValue()
  198. }
  199. pathsObj, err := call.Argument(1).Export()
  200. if err != nil {
  201. g.RaiseError(err)
  202. return otto.FalseValue()
  203. }
  204. destVpath, err := call.Argument(2).ToString()
  205. if err != nil {
  206. g.RaiseError(err)
  207. return otto.FalseValue()
  208. }
  209. zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
  210. destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
  211. targetPaths := szParsePathsArg(pathsObj)
  212. if targetPaths == nil {
  213. g.RaiseError(errors.New("invalid paths format"))
  214. return otto.FalseValue()
  215. }
  216. if !u.CanRead(zipVpath) {
  217. panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
  218. }
  219. if !u.CanWrite(destVpath) {
  220. panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
  221. }
  222. _, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
  223. if err != nil {
  224. g.RaiseError(err)
  225. return otto.FalseValue()
  226. }
  227. destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
  228. if err != nil {
  229. g.RaiseError(err)
  230. return otto.FalseValue()
  231. }
  232. r, err := sevenzip.OpenReader(zipRpath)
  233. if err != nil {
  234. g.RaiseError(err)
  235. return otto.FalseValue()
  236. }
  237. defer r.Close()
  238. for _, f := range r.File {
  239. outRelPath := szMatchPartialPath(filepath.ToSlash(f.Name), targetPaths)
  240. if outRelPath == "" {
  241. continue
  242. }
  243. outPath := filepath.Join(destRpath, filepath.FromSlash(outRelPath))
  244. if f.FileInfo().IsDir() {
  245. destFsh.FileSystemAbstraction.MkdirAll(outPath, 0755)
  246. continue
  247. }
  248. destFsh.FileSystemAbstraction.MkdirAll(filepath.Dir(outPath), 0755)
  249. rc, err := f.Open()
  250. if err != nil {
  251. continue
  252. }
  253. destFsh.FileSystemAbstraction.WriteStream(outPath, rc, 0755)
  254. rc.Close()
  255. vp, err := static.RealpathToVirtualpath(destFsh, outPath, u)
  256. if err == nil {
  257. u.SetOwnerOfFile(destFsh, vp)
  258. }
  259. }
  260. reply, _ := vm.ToValue(true)
  261. return reply
  262. })
  263. // get7zFileInfo(srcVpath) --> JSON string {fileCount, dirCount, totalUncompressedSize, totalCompressedSize}
  264. vm.Set("_ziplib_get7zFileInfo", func(call otto.FunctionCall) otto.Value {
  265. zipVpath, err := call.Argument(0).ToString()
  266. if err != nil {
  267. g.RaiseError(err)
  268. return otto.NullValue()
  269. }
  270. zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
  271. if !u.CanRead(zipVpath) {
  272. panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
  273. }
  274. _, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
  275. if err != nil {
  276. g.RaiseError(err)
  277. return otto.NullValue()
  278. }
  279. jsonStr, err := sz7zGetFileInfo(zipRpath)
  280. if err != nil {
  281. g.RaiseError(err)
  282. return otto.NullValue()
  283. }
  284. reply, _ := vm.ToValue(jsonStr)
  285. return reply
  286. })
  287. vm.Run(`
  288. // 7z-specific functions
  289. ziplib.extract7zFile = _ziplib_extract7zFile; // Extract all files from a 7z archive
  290. ziplib.list7zFileDir = _ziplib_list7zFileDir; // List immediate children of a dir in 7z
  291. ziplib.list7zFileContents = _ziplib_list7zFileContents; // Full JSON tree of a 7z archive
  292. ziplib.getFileFrom7z = _ziplib_getFileFrom7z; // Extract single file from 7z to tmp:/
  293. ziplib.extractPartial7z = _ziplib_extractPartial7z; // Extract selected files/folders from 7z
  294. ziplib.get7zFileInfo = _ziplib_get7zFileInfo; // Metadata (counts, sizes) of a 7z
  295. `)
  296. }
  297. // ── Package-level helpers (shared between inject7zLibFunctions and the
  298. // dispatch branches added to existing ziplib functions) ──────────────────
  299. // szListDir returns the immediate children of dirPath inside the 7z at rpath.
  300. // Handles archives without explicit directory entries by inferring dirs from
  301. // file paths (common in 7z).
  302. func szListDir(rpath, dirPath string) ([]string, error) {
  303. r, err := sevenzip.OpenReader(rpath)
  304. if err != nil {
  305. return nil, err
  306. }
  307. defer r.Close()
  308. dirPath = filepath.ToSlash(strings.TrimPrefix(dirPath, "/"))
  309. if dirPath != "" && !strings.HasSuffix(dirPath, "/") {
  310. dirPath += "/"
  311. }
  312. var filelist []string
  313. dirExists := dirPath == ""
  314. seenItems := make(map[string]bool)
  315. for _, f := range r.File {
  316. fName := filepath.ToSlash(f.Name)
  317. if dirPath == "" {
  318. parts := strings.Split(fName, "/")
  319. if len(parts) == 0 {
  320. continue
  321. }
  322. item := parts[0]
  323. if item == "" || seenItems[item] {
  324. continue
  325. }
  326. seenItems[item] = true
  327. if len(parts) > 1 || f.FileInfo().IsDir() {
  328. filelist = append(filelist, item+"/")
  329. } else {
  330. filelist = append(filelist, item)
  331. }
  332. } else if strings.HasPrefix(fName, dirPath) {
  333. dirExists = true
  334. relPath := strings.TrimPrefix(fName, dirPath)
  335. if relPath == "" {
  336. continue
  337. }
  338. parts := strings.Split(relPath, "/")
  339. item := parts[0]
  340. if item == "" || seenItems[item] {
  341. continue
  342. }
  343. seenItems[item] = true
  344. if len(parts) > 1 || strings.HasSuffix(relPath, "/") || f.FileInfo().IsDir() {
  345. filelist = append(filelist, item+"/")
  346. } else {
  347. filelist = append(filelist, item)
  348. }
  349. }
  350. }
  351. if dirPath != "" && !dirExists {
  352. return nil, errors.New("Directory not found in archive: " + dirPath)
  353. }
  354. return filelist, nil
  355. }
  356. // sz7zListContentsJSON builds a JSON tree (same schema as listZipFileContents)
  357. // from all entries in the 7z archive at rpath.
  358. func sz7zListContentsJSON(rpath string) (string, error) {
  359. r, err := sevenzip.OpenReader(rpath)
  360. if err != nil {
  361. return "", err
  362. }
  363. defer r.Close()
  364. type Node struct {
  365. Name string `json:"name"`
  366. IsDir bool `json:"isDir"`
  367. Size int64 `json:"size"`
  368. Children map[string]*Node `json:"children,omitempty"`
  369. }
  370. root := &Node{Name: "/", IsDir: true, Children: make(map[string]*Node)}
  371. for _, f := range r.File {
  372. parts := strings.Split(filepath.ToSlash(f.Name), "/")
  373. current := root
  374. for i, part := range parts {
  375. if part == "" {
  376. continue
  377. }
  378. isLast := i == len(parts)-1
  379. if current.Children == nil {
  380. current.Children = make(map[string]*Node)
  381. }
  382. if _, exists := current.Children[part]; !exists {
  383. current.Children[part] = &Node{
  384. Name: part,
  385. IsDir: !isLast || f.FileInfo().IsDir(),
  386. }
  387. if isLast && !f.FileInfo().IsDir() {
  388. current.Children[part].Size = f.FileInfo().Size()
  389. }
  390. }
  391. current = current.Children[part]
  392. }
  393. }
  394. data, err := json.Marshal(root)
  395. if err != nil {
  396. return "", err
  397. }
  398. return string(data), nil
  399. }
  400. // sz7zGetFileInfo returns JSON metadata for the 7z archive at rpath.
  401. func sz7zGetFileInfo(rpath string) (string, error) {
  402. r, err := sevenzip.OpenReader(rpath)
  403. if err != nil {
  404. return "", err
  405. }
  406. defer r.Close()
  407. type Info struct {
  408. FileCount int `json:"fileCount"`
  409. DirCount int `json:"dirCount"`
  410. TotalUncompressedSize int64 `json:"totalUncompressedSize"`
  411. TotalCompressedSize int64 `json:"totalCompressedSize"` // always 0: 7z uses solid compression
  412. }
  413. info := Info{}
  414. for _, f := range r.File {
  415. if f.FileInfo().IsDir() {
  416. info.DirCount++
  417. } else {
  418. info.FileCount++
  419. info.TotalUncompressedSize += f.FileInfo().Size()
  420. }
  421. }
  422. data, err := json.Marshal(info)
  423. if err != nil {
  424. return "", err
  425. }
  426. return string(data), nil
  427. }
  428. // sz7zExtractAll extracts every entry in the 7z at srcRpath into destRpath
  429. // using the provided filesystem handler.
  430. func sz7zExtractAll(srcRpath string, destFsh *filesystem.FileSystemHandler, destRpath string) error {
  431. r, err := sevenzip.OpenReader(srcRpath)
  432. if err != nil {
  433. return err
  434. }
  435. defer r.Close()
  436. for _, f := range r.File {
  437. fName := filepath.ToSlash(f.Name)
  438. outPath := filepath.Join(destRpath, filepath.FromSlash(fName))
  439. if f.FileInfo().IsDir() {
  440. destFsh.FileSystemAbstraction.MkdirAll(outPath, 0755)
  441. continue
  442. }
  443. destFsh.FileSystemAbstraction.MkdirAll(filepath.Dir(outPath), 0755)
  444. rc, err := f.Open()
  445. if err != nil {
  446. continue
  447. }
  448. destFsh.FileSystemAbstraction.WriteStream(outPath, rc, 0755)
  449. rc.Close()
  450. }
  451. return nil
  452. }
  453. // szParsePathsArg converts the second argument of extractPartial7z / extractPartialZip
  454. // (JS array or raw JSON string) into a []string. Returns nil on unrecognised type.
  455. func szParsePathsArg(pathsObj interface{}) []string {
  456. var result []string
  457. switch v := pathsObj.(type) {
  458. case []string:
  459. // Otto exports an array whose items are all strings as []string
  460. for _, s := range v {
  461. result = append(result, filepath.ToSlash(strings.TrimPrefix(s, "/")))
  462. }
  463. case []interface{}:
  464. for _, s := range v {
  465. if str, ok := s.(string); ok {
  466. result = append(result, filepath.ToSlash(strings.TrimPrefix(str, "/")))
  467. }
  468. }
  469. case string:
  470. v = strings.TrimSpace(v)
  471. if strings.HasPrefix(v, "[") {
  472. var arr []string
  473. if json.Unmarshal([]byte(v), &arr) == nil {
  474. for _, s := range arr {
  475. result = append(result, filepath.ToSlash(strings.TrimPrefix(s, "/")))
  476. }
  477. return result
  478. }
  479. }
  480. if v != "" {
  481. result = append(result, filepath.ToSlash(strings.TrimPrefix(v, "/")))
  482. }
  483. default:
  484. return nil
  485. }
  486. return result
  487. }
  488. // szMatchPartialPath returns the output-relative path for fName given the list
  489. // of selected targetPaths, applying the same strip-parent-for-folders /
  490. // flatten-for-files semantics as extractPartialZip. Returns "" if not matched.
  491. func szMatchPartialPath(fName string, targetPaths []string) string {
  492. for _, target := range targetPaths {
  493. if target == "" || target == "/" {
  494. return fName
  495. }
  496. isFolderTarget := strings.HasSuffix(target, "/")
  497. normalizedTarget := strings.TrimSuffix(target, "/")
  498. if isFolderTarget {
  499. if strings.HasPrefix(fName, target) || fName == target || fName == normalizedTarget {
  500. lastSlash := strings.LastIndex(normalizedTarget, "/")
  501. parent := ""
  502. if lastSlash >= 0 {
  503. parent = normalizedTarget[:lastSlash+1]
  504. }
  505. return strings.TrimPrefix(fName, parent)
  506. }
  507. } else {
  508. if fName == target {
  509. return filepath.Base(fName)
  510. }
  511. if strings.HasPrefix(fName, normalizedTarget+"/") {
  512. lastSlash := strings.LastIndex(normalizedTarget, "/")
  513. parent := ""
  514. if lastSlash >= 0 {
  515. parent = normalizedTarget[:lastSlash+1]
  516. }
  517. return strings.TrimPrefix(fName, parent)
  518. }
  519. }
  520. }
  521. return ""
  522. }