• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5package codehost
6
7import (
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	"cmd/go/internal/base"
20)
21
22func 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
47func 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}
169