All large file download and upload testing

This commit is contained in:
Chun-Hung Tseng
2023-06-26 23:37:40 +02:00
parent 37252ec62a
commit 89beb44c28
6 changed files with 112 additions and 77 deletions

View File

@@ -55,8 +55,9 @@ Currently, the development are split into 2 versions. V1 supports the features [
- [ ] File actions
- [x] Download
- [x] Download empty file
- [ ] Properly handle large files and empty files (check iOS codebase)
- [x] Properly handle large files and empty files (check iOS codebase)
- esp. large files, where buffering in-memory will screw up the runtime
- [ ] Improve large file handling
- [ ] Check signature and hash
- [x] Delete
- [x] Upload
@@ -64,8 +65,7 @@ Currently, the development are split into 2 versions. V1 supports the features [
- [x] Parse mime type
- [x] Add revision
- [x] Modified time
- [ ] Improve to handle large files
- [ ] Upload verification
- [ ] Improve large file handling
- [ ] Handle failed / interrupted upload
- [ ] List file metadata
- [x] Duplicated file name handling: 422: A file or folder with that name already exists (Code=2500, Status=422)
@@ -107,7 +107,6 @@ Currently, the development are split into 2 versions. V1 supports the features [
- [x] Remove delete all's hardcoded string
- [ ] Address TODO and FIXME
- [ ] Use CI to run integration tests
- [ ] Figure out the bottleneck by doing some profiling
- [ ] Some error handling from [here](https://github.com/ProtonMail/WebClients/blob/main/packages/shared/lib/drive/constants.ts) MAX_NAME_LENGTH, TIMEOUT
- [x] Point to the right proton-go-api branch
- [x] Run `go get github.com/henrybear327/go-proton-api@dev` to update go mod
@@ -125,6 +124,7 @@ Currently, the development are split into 2 versions. V1 supports the features [
Moving files and folders are [features](https://github.com/rclone/rclone/blob/51a468b2bae4ca8e21760435211623a8199a9167/fs/features.go#L25)
- [ ] Figure out the bottleneck by doing some profiling
- [ ] Folder
- [ ] (Feature) Update (force overwrite)
- [ ] (Feature) Move

View File

@@ -3,6 +3,7 @@ package proton_api_bridge
import (
"context"
"log"
"strings"
"testing"
"github.com/henrybear327/Proton-API-Bridge/common"
@@ -78,15 +79,15 @@ func TestUploadAndDownloadAndDeleteAFile(t *testing.T) {
})
log.Println("Upload integrationTestImage.png")
uploadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png")
uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png")
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1)
checkFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"})
downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png")
downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", "")
log.Println("Upload a new revision to replace integrationTestImage.png")
uploadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2)
downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png")
downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", "")
checkFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"})
log.Println("Delete file integrationTestImage.png")
@@ -102,15 +103,15 @@ func TestUploadAndDeleteAnEmptyFileAtRoot(t *testing.T) {
})
log.Println("Upload empty.txt")
uploadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt")
uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt")
checkRevisions(protonDrive, ctx, t, "empty.txt", 1)
checkFileListing(t, ctx, protonDrive, []string{"/empty.txt"})
downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt")
downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", "")
log.Println("Upload a new revision to replace empty.txt")
uploadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt") /* Add a revision */
uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt") /* Add a revision */
checkRevisions(protonDrive, ctx, t, "empty.txt", 2)
downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt")
downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", "")
checkFileListing(t, ctx, protonDrive, []string{"/empty.txt"})
log.Println("Delete file empty.txt")
@@ -130,15 +131,15 @@ func TestUploadAndDownloadAndDeleteAFileAtAFolderOneLevelFromRoot(t *testing.T)
checkFileListing(t, ctx, protonDrive, []string{"/level1"})
log.Println("Upload integrationTestImage.png to level1")
uploadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png")
uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png")
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1)
checkFileListing(t, ctx, protonDrive, []string{"/level1", "/level1/integrationTestImage.png"})
downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png")
downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png", "")
log.Println("Upload a new revision to replace integrationTestImage.png in level1")
uploadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2)
downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png")
downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png", "")
log.Println("Delete folder level1")
deleteBySearchingFromRoot(t, ctx, protonDrive, "level1", true)
@@ -181,19 +182,19 @@ func TestCreateAndMoveAndDeleteFolderWithAFile(t *testing.T) {
checkFileListing(t, ctx, protonDrive, []string{"/src"})
log.Println("Upload integrationTestImage.png to src")
uploadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png")
uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png")
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1)
checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"})
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png")
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", "")
log.Println("Create a folder dst at root")
createFolder(t, ctx, protonDrive, "", "dst")
checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"})
log.Println("Upload a new revision to replace integrationTestImage.png in src")
uploadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2)
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png")
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", "")
checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"})
log.Println("Move folder src to under folder dst")
@@ -217,19 +218,19 @@ func TestCreateAndMoveAndDeleteAFileOneLevelFromRoot(t *testing.T) {
checkFileListing(t, ctx, protonDrive, []string{"/src"})
log.Println("Upload integrationTestImage.png to src")
uploadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png")
uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png")
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1)
checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"})
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png")
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", "")
log.Println("Create a folder dst at root")
createFolder(t, ctx, protonDrive, "", "dst")
checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"})
log.Println("Upload a new revision to replace integrationTestImage.png in src")
uploadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2)
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png")
downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", "")
checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"})
log.Println("Move folder src to under folder dst")
@@ -241,39 +242,43 @@ func TestCreateAndMoveAndDeleteAFileOneLevelFromRoot(t *testing.T) {
checkFileListing(t, ctx, protonDrive, []string{"/src"})
}
// func TestUploadLargeNumberOfBlocks(t *testing.T) {
// ctx, cancel, protonDrive := setup(t)
// t.Cleanup(func() {
// defer cancel()
// defer tearDown(t, ctx, protonDrive)
// })
func TestUploadLargeNumberOfBlocks(t *testing.T) {
ctx, cancel, protonDrive := setup(t)
t.Cleanup(func() {
defer cancel()
defer tearDown(t, ctx, protonDrive)
})
// // in order to simulate uploading large files
// // we use 1KB for the UPLOAD_BLOCK_SIZE
// // so a 1000KB file will generate 1000 blocks to test the uploading mechanism
// // and also testing the downloading mechanism
// ORIGINAL_UPLOAD_BLOCK_SIZE := UPLOAD_BLOCK_SIZE
// defer func() {
// UPLOAD_BLOCK_SIZE = ORIGINAL_UPLOAD_BLOCK_SIZE
// }()
// blocks := 500
// UPLOAD_BLOCK_SIZE = 1000
// in order to simulate uploading large files
// we use 1KB for the UPLOAD_BLOCK_SIZE
// so a 1000KB file will generate 1000 blocks to test the uploading mechanism
// and also testing the downloading mechanism
ORIGINAL_UPLOAD_BLOCK_SIZE := UPLOAD_BLOCK_SIZE
defer func() {
UPLOAD_BLOCK_SIZE = ORIGINAL_UPLOAD_BLOCK_SIZE
}()
blocks := 100
UPLOAD_BLOCK_SIZE = 10
// file1Content := RandomString(UPLOAD_BLOCK_SIZE * blocks)
filename := "fileContent.txt"
file1Content := RandomString(UPLOAD_BLOCK_SIZE * blocks)
file1ContentReader := strings.NewReader(file1Content)
file2Content := RandomString(UPLOAD_BLOCK_SIZE * blocks)
file2ContentReader := strings.NewReader(file2Content)
// log.Println("Upload file1Content")
// uploadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png")
// checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1)
// checkFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"})
// downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png")
log.Println("Upload fileContent.txt")
uploadFileByReader(t, ctx, protonDrive, "", filename, file1ContentReader)
checkRevisions(protonDrive, ctx, t, filename, 1)
checkFileListing(t, ctx, protonDrive, []string{"/" + filename})
downloadFile(t, ctx, protonDrive, "", filename, "", file1Content)
// log.Println("Upload a new revision to replace integrationTestImage.png")
// uploadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */
// checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2)
// downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png")
// checkFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"})
log.Println("Upload a new revision to replace fileContent.txt")
uploadFileByReader(t, ctx, protonDrive, "", filename, file2ContentReader)
checkRevisions(protonDrive, ctx, t, filename, 2)
checkFileListing(t, ctx, protonDrive, []string{"/" + filename})
downloadFile(t, ctx, protonDrive, "", filename, "", file2Content)
// log.Println("Delete file integrationTestImage.png")
// deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false)
// checkFileListing(t, ctx, protonDrive, []string{})
// }
log.Println("Delete file fileContent.txt")
deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false)
checkFileListing(t, ctx, protonDrive, []string{})
}

View File

@@ -4,8 +4,10 @@ import (
"bufio"
"bytes"
"context"
"io"
"os"
"testing"
"time"
"github.com/henrybear327/go-proton-api"
@@ -66,7 +68,29 @@ func createFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p
}
}
func uploadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string) {
func uploadFileByReader(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, in io.Reader) {
parentLink := protonDrive.RootLink
if parent != "" {
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true)
if err != nil {
t.Fatal(err)
}
if targetFolderLink == nil {
t.Fatalf("Folder %v not found", parent)
}
parentLink = targetFolderLink
}
if parentLink.Type != proton.LinkTypeFolder {
t.Fatalf("parentLink is not of folder type")
}
_, _, err := protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, time.Now(), in)
if err != nil {
t.Fatal(err)
}
}
func uploadFileByFilepath(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string) {
parentLink := protonDrive.RootLink
if parent != "" {
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true)
@@ -101,7 +125,7 @@ func uploadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, par
}
}
func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string) {
func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, data string) {
parentLink := protonDrive.RootLink
if parent != "" {
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true)
@@ -135,17 +159,25 @@ func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p
t.Fatalf("FileSystemAttr should not be nil")
} else {
if len(downloadedData) != int(fileSystemAttr.Size) {
t.Fatalf("Downloaded file size != uploaded file size: %#v", fileSystemAttr)
t.Fatalf("Downloaded file size != uploaded file size: %#v vs %#v", len(downloadedData), int(fileSystemAttr.Size))
}
}
originalData, err := os.ReadFile(filepath)
if err != nil {
t.Fatal(err)
}
if filepath != "" {
originalData, err := os.ReadFile(filepath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(downloadedData, originalData) {
t.Fatalf("Downloaded content is different from the original content")
if !bytes.Equal(downloadedData, originalData) {
t.Fatalf("Downloaded content is different from the original content")
}
} else if data != "" {
if !bytes.Equal(downloadedData, []byte(data)) {
t.Fatalf("Downloaded content is different from the original content")
}
} else {
t.Fatalf("Nothing to verify against")
}
}
}

View File

@@ -47,13 +47,11 @@ func (protonDrive *ProtonDrive) GetActiveRevision(ctx context.Context, link *pro
}
}
// FIXME: compute total blocks required
// TODO: handle large file downloading
// FIXME: total block calculation: how?
revision, err := protonDrive.c.GetRevision(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisions[activeRevision].ID, 1, 50)
revision, err := protonDrive.c.GetRevisionAllBlocks(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisions[activeRevision].ID)
if err != nil {
return nil, err
}
// log.Println("Total blocks", len(revision.Blocks))
return &revision, nil
}

6
go.mod
View File

@@ -5,7 +5,7 @@ go 1.18
require (
github.com/ProtonMail/gopenpgp/v2 v2.7.1
github.com/gabriel-vasile/mimetype v1.4.2
github.com/henrybear327/go-proton-api v0.0.0-20230626095836-c05921e07e64
github.com/henrybear327/go-proton-api v0.0.0-20230626213659-c3c0905ee910
github.com/relvacode/iso8601 v1.3.0
)
@@ -22,13 +22,13 @@ require (
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/emersion/go-message v0.16.0 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
github.com/emersion/go-vcard v0.0.0-20230626131229-38c18b295bbd // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.9.0 // indirect

12
go.sum
View File

@@ -36,8 +36,8 @@ github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fK
github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-vcard v0.0.0-20230626131229-38c18b295bbd h1:n1kH4lDJLDgO8sqkt0QgeQXKims1L8khdgilk9G5lm8=
github.com/emersion/go-vcard v0.0.0-20230626131229-38c18b295bbd/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -50,8 +50,8 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/henrybear327/go-proton-api v0.0.0-20230626095836-c05921e07e64 h1:23dMmB9tttnzpli/aswiujO+/uj8Uuxh9jRSenOnQnE=
github.com/henrybear327/go-proton-api v0.0.0-20230626095836-c05921e07e64/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
github.com/henrybear327/go-proton-api v0.0.0-20230626213659-c3c0905ee910 h1:FZIVc2R3G2dY+c1u2KYiXaBx1IiLfmp6ir5cm9ityPs=
github.com/henrybear327/go-proton-api v0.0.0-20230626213659-c3c0905ee910/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
@@ -81,8 +81,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=