github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modfetch/codehost/svn.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package codehost 6 7 import ( 8 "archive/zip" 9 "context" 10 "encoding/xml" 11 "fmt" 12 "io" 13 "os" 14 "path" 15 "path/filepath" 16 "strconv" 17 "time" 18 19 "github.com/go-asm/go/cmd/go/base" 20 ) 21 22 func svnParseStat(rev, out string) (*RevInfo, error) { 23 var log struct { 24 Logentry struct { 25 Revision int64 `xml:"revision,attr"` 26 Date string `xml:"date"` 27 } `xml:"logentry"` 28 } 29 if err := xml.Unmarshal([]byte(out), &log); err != nil { 30 return nil, vcsErrorf("unexpected response from svn log --xml: %v\n%s", err, out) 31 } 32 33 t, err := time.Parse(time.RFC3339, log.Logentry.Date) 34 if err != nil { 35 return nil, vcsErrorf("unexpected response from svn log --xml: %v\n%s", err, out) 36 } 37 38 info := &RevInfo{ 39 Name: strconv.FormatInt(log.Logentry.Revision, 10), 40 Short: fmt.Sprintf("%012d", log.Logentry.Revision), 41 Time: t.UTC(), 42 Version: rev, 43 } 44 return info, nil 45 } 46 47 func svnReadZip(ctx context.Context, dst io.Writer, workDir, rev, subdir, remote string) (err error) { 48 // The subversion CLI doesn't provide a command to write the repository 49 // directly to an archive, so we need to export it to the local filesystem 50 // instead. Unfortunately, the local filesystem might apply arbitrary 51 // normalization to the filenames, so we need to obtain those directly. 52 // 53 // 'svn export' prints the filenames as they are written, but from reading the 54 // svn source code (as of revision 1868933), those filenames are encoded using 55 // the system locale rather than preserved byte-for-byte from the origin. For 56 // our purposes, that won't do, but we don't want to go mucking around with 57 // the user's locale settings either — that could impact error messages, and 58 // we don't know what locales the user has available or what LC_* variables 59 // their platform supports. 60 // 61 // Instead, we'll do a two-pass export: first we'll run 'svn list' to get the 62 // canonical filenames, then we'll 'svn export' and look for those filenames 63 // in the local filesystem. (If there is an encoding problem at that point, we 64 // would probably reject the resulting module anyway.) 65 66 remotePath := remote 67 if subdir != "" { 68 remotePath += "/" + subdir 69 } 70 71 release, err := base.AcquireNet() 72 if err != nil { 73 return err 74 } 75 out, err := Run(ctx, workDir, []string{ 76 "svn", "list", 77 "--non-interactive", 78 "--xml", 79 "--incremental", 80 "--recursive", 81 "--revision", rev, 82 "--", remotePath, 83 }) 84 release() 85 if err != nil { 86 return err 87 } 88 89 type listEntry struct { 90 Kind string `xml:"kind,attr"` 91 Name string `xml:"name"` 92 Size int64 `xml:"size"` 93 } 94 var list struct { 95 Entries []listEntry `xml:"entry"` 96 } 97 if err := xml.Unmarshal(out, &list); err != nil { 98 return vcsErrorf("unexpected response from svn list --xml: %v\n%s", err, out) 99 } 100 101 exportDir := filepath.Join(workDir, "export") 102 // Remove any existing contents from a previous (failed) run. 103 if err := os.RemoveAll(exportDir); err != nil { 104 return err 105 } 106 defer os.RemoveAll(exportDir) // best-effort 107 108 release, err = base.AcquireNet() 109 if err != nil { 110 return err 111 } 112 _, err = Run(ctx, workDir, []string{ 113 "svn", "export", 114 "--non-interactive", 115 "--quiet", 116 117 // Suppress any platform- or host-dependent transformations. 118 "--native-eol", "LF", 119 "--ignore-externals", 120 "--ignore-keywords", 121 122 "--revision", rev, 123 "--", remotePath, 124 exportDir, 125 }) 126 release() 127 if err != nil { 128 return err 129 } 130 131 // Scrape the exported files out of the filesystem and encode them in the zipfile. 132 133 // “All files in the zip file are expected to be 134 // nested in a single top-level directory, whose name is not specified.” 135 // We'll (arbitrarily) choose the base of the remote path. 136 basePath := path.Join(path.Base(remote), subdir) 137 138 zw := zip.NewWriter(dst) 139 for _, e := range list.Entries { 140 if e.Kind != "file" { 141 continue 142 } 143 144 zf, err := zw.Create(path.Join(basePath, e.Name)) 145 if err != nil { 146 return err 147 } 148 149 f, err := os.Open(filepath.Join(exportDir, e.Name)) 150 if err != nil { 151 if os.IsNotExist(err) { 152 return vcsErrorf("file reported by 'svn list', but not written by 'svn export': %s", e.Name) 153 } 154 return fmt.Errorf("error opening file created by 'svn export': %v", err) 155 } 156 157 n, err := io.Copy(zf, f) 158 f.Close() 159 if err != nil { 160 return err 161 } 162 if n != e.Size { 163 return vcsErrorf("file size differs between 'svn list' and 'svn export': file %s listed as %v bytes, but exported as %v bytes", e.Name, e.Size, n) 164 } 165 } 166 167 return zw.Close() 168 }