github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/modfetch/codehost/vcs.go (about)

     1  // Copyright 2018 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  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/fs"
    13  	"os"
    14  	"path/filepath"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"time"
    20  
    21  	"github.com/go-asm/go/lazyregexp"
    22  
    23  	"github.com/go-asm/go/cmd/go/base"
    24  	"github.com/go-asm/go/cmd/go/lockedfile"
    25  	"github.com/go-asm/go/cmd/go/par"
    26  	"github.com/go-asm/go/cmd/go/str"
    27  )
    28  
    29  // A VCSError indicates an error using a version control system.
    30  // The implication of a VCSError is that we know definitively where
    31  // to get the code, but we can't access it due to the error.
    32  // The caller should report this error instead of continuing to probe
    33  // other possible module paths.
    34  //
    35  // TODO(golang.org/issue/31730): See if we can invert this. (Return a
    36  // distinguished error for “repo not found” and treat everything else
    37  // as terminal.)
    38  type VCSError struct {
    39  	Err error
    40  }
    41  
    42  func (e *VCSError) Error() string { return e.Err.Error() }
    43  
    44  func (e *VCSError) Unwrap() error { return e.Err }
    45  
    46  func vcsErrorf(format string, a ...any) error {
    47  	return &VCSError{Err: fmt.Errorf(format, a...)}
    48  }
    49  
    50  type vcsCacheKey struct {
    51  	vcs    string
    52  	remote string
    53  }
    54  
    55  func NewRepo(ctx context.Context, vcs, remote string) (Repo, error) {
    56  	return vcsRepoCache.Do(vcsCacheKey{vcs, remote}, func() (Repo, error) {
    57  		repo, err := newVCSRepo(ctx, vcs, remote)
    58  		if err != nil {
    59  			return nil, &VCSError{err}
    60  		}
    61  		return repo, nil
    62  	})
    63  }
    64  
    65  var vcsRepoCache par.ErrCache[vcsCacheKey, Repo]
    66  
    67  type vcsRepo struct {
    68  	mu lockedfile.Mutex // protects all commands, so we don't have to decide which are safe on a per-VCS basis
    69  
    70  	remote string
    71  	cmd    *vcsCmd
    72  	dir    string
    73  
    74  	tagsOnce sync.Once
    75  	tags     map[string]bool
    76  
    77  	branchesOnce sync.Once
    78  	branches     map[string]bool
    79  
    80  	fetchOnce sync.Once
    81  	fetchErr  error
    82  }
    83  
    84  func newVCSRepo(ctx context.Context, vcs, remote string) (Repo, error) {
    85  	if vcs == "git" {
    86  		return newGitRepo(ctx, remote, false)
    87  	}
    88  	cmd := vcsCmds[vcs]
    89  	if cmd == nil {
    90  		return nil, fmt.Errorf("unknown vcs: %s %s", vcs, remote)
    91  	}
    92  	if !strings.Contains(remote, "://") {
    93  		return nil, fmt.Errorf("invalid vcs remote: %s %s", vcs, remote)
    94  	}
    95  
    96  	r := &vcsRepo{remote: remote, cmd: cmd}
    97  	var err error
    98  	r.dir, r.mu.Path, err = WorkDir(ctx, vcsWorkDirType+vcs, r.remote)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	if cmd.init == nil {
   104  		return r, nil
   105  	}
   106  
   107  	unlock, err := r.mu.Lock()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	defer unlock()
   112  
   113  	if _, err := os.Stat(filepath.Join(r.dir, "."+vcs)); err != nil {
   114  		release, err := base.AcquireNet()
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  		_, err = Run(ctx, r.dir, cmd.init(r.remote))
   119  		release()
   120  
   121  		if err != nil {
   122  			os.RemoveAll(r.dir)
   123  			return nil, err
   124  		}
   125  	}
   126  	return r, nil
   127  }
   128  
   129  const vcsWorkDirType = "vcs1."
   130  
   131  type vcsCmd struct {
   132  	vcs           string                                                                              // vcs name "hg"
   133  	init          func(remote string) []string                                                        // cmd to init repo to track remote
   134  	tags          func(remote string) []string                                                        // cmd to list local tags
   135  	tagRE         *lazyregexp.Regexp                                                                  // regexp to extract tag names from output of tags cmd
   136  	branches      func(remote string) []string                                                        // cmd to list local branches
   137  	branchRE      *lazyregexp.Regexp                                                                  // regexp to extract branch names from output of tags cmd
   138  	badLocalRevRE *lazyregexp.Regexp                                                                  // regexp of names that must not be served out of local cache without doing fetch first
   139  	statLocal     func(rev, remote string) []string                                                   // cmd to stat local rev
   140  	parseStat     func(rev, out string) (*RevInfo, error)                                             // cmd to parse output of statLocal
   141  	fetch         []string                                                                            // cmd to fetch everything from remote
   142  	latest        string                                                                              // name of latest commit on remote (tip, HEAD, etc)
   143  	readFile      func(rev, file, remote string) []string                                             // cmd to read rev's file
   144  	readZip       func(rev, subdir, remote, target string) []string                                   // cmd to read rev's subdir as zip file
   145  	doReadZip     func(ctx context.Context, dst io.Writer, workDir, rev, subdir, remote string) error // arbitrary function to read rev's subdir as zip file
   146  }
   147  
   148  var re = lazyregexp.New
   149  
   150  var vcsCmds = map[string]*vcsCmd{
   151  	"hg": {
   152  		vcs: "hg",
   153  		init: func(remote string) []string {
   154  			return []string{"hg", "clone", "-U", "--", remote, "."}
   155  		},
   156  		tags: func(remote string) []string {
   157  			return []string{"hg", "tags", "-q"}
   158  		},
   159  		tagRE: re(`(?m)^[^\n]+$`),
   160  		branches: func(remote string) []string {
   161  			return []string{"hg", "branches", "-c", "-q"}
   162  		},
   163  		branchRE:      re(`(?m)^[^\n]+$`),
   164  		badLocalRevRE: re(`(?m)^(tip)$`),
   165  		statLocal: func(rev, remote string) []string {
   166  			return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"}
   167  		},
   168  		parseStat: hgParseStat,
   169  		fetch:     []string{"hg", "pull", "-f"},
   170  		latest:    "tip",
   171  		readFile: func(rev, file, remote string) []string {
   172  			return []string{"hg", "cat", "-r", rev, file}
   173  		},
   174  		readZip: func(rev, subdir, remote, target string) []string {
   175  			pattern := []string{}
   176  			if subdir != "" {
   177  				pattern = []string{"-I", subdir + "/**"}
   178  			}
   179  			return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target)
   180  		},
   181  	},
   182  
   183  	"svn": {
   184  		vcs:  "svn",
   185  		init: nil, // no local checkout
   186  		tags: func(remote string) []string {
   187  			return []string{"svn", "list", "--", strings.TrimSuffix(remote, "/trunk") + "/tags"}
   188  		},
   189  		tagRE: re(`(?m)^(.*?)/?$`),
   190  		statLocal: func(rev, remote string) []string {
   191  			suffix := "@" + rev
   192  			if rev == "latest" {
   193  				suffix = ""
   194  			}
   195  			return []string{"svn", "log", "-l1", "--xml", "--", remote + suffix}
   196  		},
   197  		parseStat: svnParseStat,
   198  		latest:    "latest",
   199  		readFile: func(rev, file, remote string) []string {
   200  			return []string{"svn", "cat", "--", remote + "/" + file + "@" + rev}
   201  		},
   202  		doReadZip: svnReadZip,
   203  	},
   204  
   205  	"bzr": {
   206  		vcs: "bzr",
   207  		init: func(remote string) []string {
   208  			return []string{"bzr", "branch", "--use-existing-dir", "--", remote, "."}
   209  		},
   210  		fetch: []string{
   211  			"bzr", "pull", "--overwrite-tags",
   212  		},
   213  		tags: func(remote string) []string {
   214  			return []string{"bzr", "tags"}
   215  		},
   216  		tagRE:         re(`(?m)^\S+`),
   217  		badLocalRevRE: re(`^revno:-`),
   218  		statLocal: func(rev, remote string) []string {
   219  			return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev}
   220  		},
   221  		parseStat: bzrParseStat,
   222  		latest:    "revno:-1",
   223  		readFile: func(rev, file, remote string) []string {
   224  			return []string{"bzr", "cat", "-r", rev, file}
   225  		},
   226  		readZip: func(rev, subdir, remote, target string) []string {
   227  			extra := []string{}
   228  			if subdir != "" {
   229  				extra = []string{"./" + subdir}
   230  			}
   231  			return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra)
   232  		},
   233  	},
   234  
   235  	"fossil": {
   236  		vcs: "fossil",
   237  		init: func(remote string) []string {
   238  			return []string{"fossil", "clone", "--", remote, ".fossil"}
   239  		},
   240  		fetch: []string{"fossil", "pull", "-R", ".fossil"},
   241  		tags: func(remote string) []string {
   242  			return []string{"fossil", "tag", "-R", ".fossil", "list"}
   243  		},
   244  		tagRE: re(`XXXTODO`),
   245  		statLocal: func(rev, remote string) []string {
   246  			return []string{"fossil", "info", "-R", ".fossil", rev}
   247  		},
   248  		parseStat: fossilParseStat,
   249  		latest:    "trunk",
   250  		readFile: func(rev, file, remote string) []string {
   251  			return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file}
   252  		},
   253  		readZip: func(rev, subdir, remote, target string) []string {
   254  			extra := []string{}
   255  			if subdir != "" && !strings.ContainsAny(subdir, "*?[],") {
   256  				extra = []string{"--include", subdir}
   257  			}
   258  			// Note that vcsRepo.ReadZip below rewrites this command
   259  			// to run in a different directory, to work around a fossil bug.
   260  			return str.StringList("fossil", "zip", "-R", ".fossil", "--name", "prefix", extra, "--", rev, target)
   261  		},
   262  	},
   263  }
   264  
   265  func (r *vcsRepo) loadTags(ctx context.Context) {
   266  	out, err := Run(ctx, r.dir, r.cmd.tags(r.remote))
   267  	if err != nil {
   268  		return
   269  	}
   270  
   271  	// Run tag-listing command and extract tags.
   272  	r.tags = make(map[string]bool)
   273  	for _, tag := range r.cmd.tagRE.FindAllString(string(out), -1) {
   274  		if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(tag) {
   275  			continue
   276  		}
   277  		r.tags[tag] = true
   278  	}
   279  }
   280  
   281  func (r *vcsRepo) loadBranches(ctx context.Context) {
   282  	if r.cmd.branches == nil {
   283  		return
   284  	}
   285  
   286  	out, err := Run(ctx, r.dir, r.cmd.branches(r.remote))
   287  	if err != nil {
   288  		return
   289  	}
   290  
   291  	r.branches = make(map[string]bool)
   292  	for _, branch := range r.cmd.branchRE.FindAllString(string(out), -1) {
   293  		if r.cmd.badLocalRevRE != nil && r.cmd.badLocalRevRE.MatchString(branch) {
   294  			continue
   295  		}
   296  		r.branches[branch] = true
   297  	}
   298  }
   299  
   300  func (r *vcsRepo) CheckReuse(ctx context.Context, old *Origin, subdir string) error {
   301  	return fmt.Errorf("vcs %s: CheckReuse: %w", r.cmd.vcs, errors.ErrUnsupported)
   302  }
   303  
   304  func (r *vcsRepo) Tags(ctx context.Context, prefix string) (*Tags, error) {
   305  	unlock, err := r.mu.Lock()
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	defer unlock()
   310  
   311  	r.tagsOnce.Do(func() { r.loadTags(ctx) })
   312  	tags := &Tags{
   313  		// None of the other VCS provide a reasonable way to compute TagSum
   314  		// without downloading the whole repo, so we only include VCS and URL
   315  		// in the Origin.
   316  		Origin: &Origin{
   317  			VCS: r.cmd.vcs,
   318  			URL: r.remote,
   319  		},
   320  		List: []Tag{},
   321  	}
   322  	for tag := range r.tags {
   323  		if strings.HasPrefix(tag, prefix) {
   324  			tags.List = append(tags.List, Tag{tag, ""})
   325  		}
   326  	}
   327  	sort.Slice(tags.List, func(i, j int) bool {
   328  		return tags.List[i].Name < tags.List[j].Name
   329  	})
   330  	return tags, nil
   331  }
   332  
   333  func (r *vcsRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
   334  	unlock, err := r.mu.Lock()
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  	defer unlock()
   339  
   340  	if rev == "latest" {
   341  		rev = r.cmd.latest
   342  	}
   343  	r.branchesOnce.Do(func() { r.loadBranches(ctx) })
   344  	revOK := (r.cmd.badLocalRevRE == nil || !r.cmd.badLocalRevRE.MatchString(rev)) && !r.branches[rev]
   345  	if revOK {
   346  		if info, err := r.statLocal(ctx, rev); err == nil {
   347  			return info, nil
   348  		}
   349  	}
   350  
   351  	r.fetchOnce.Do(func() { r.fetch(ctx) })
   352  	if r.fetchErr != nil {
   353  		return nil, r.fetchErr
   354  	}
   355  	info, err := r.statLocal(ctx, rev)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	if !revOK {
   360  		info.Version = info.Name
   361  	}
   362  	return info, nil
   363  }
   364  
   365  func (r *vcsRepo) fetch(ctx context.Context) {
   366  	if len(r.cmd.fetch) > 0 {
   367  		release, err := base.AcquireNet()
   368  		if err != nil {
   369  			r.fetchErr = err
   370  			return
   371  		}
   372  		_, r.fetchErr = Run(ctx, r.dir, r.cmd.fetch)
   373  		release()
   374  	}
   375  }
   376  
   377  func (r *vcsRepo) statLocal(ctx context.Context, rev string) (*RevInfo, error) {
   378  	out, err := Run(ctx, r.dir, r.cmd.statLocal(rev, r.remote))
   379  	if err != nil {
   380  		return nil, &UnknownRevisionError{Rev: rev}
   381  	}
   382  	info, err := r.cmd.parseStat(rev, string(out))
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  	if info.Origin == nil {
   387  		info.Origin = new(Origin)
   388  	}
   389  	info.Origin.VCS = r.cmd.vcs
   390  	info.Origin.URL = r.remote
   391  	return info, nil
   392  }
   393  
   394  func (r *vcsRepo) Latest(ctx context.Context) (*RevInfo, error) {
   395  	return r.Stat(ctx, "latest")
   396  }
   397  
   398  func (r *vcsRepo) ReadFile(ctx context.Context, rev, file string, maxSize int64) ([]byte, error) {
   399  	if rev == "latest" {
   400  		rev = r.cmd.latest
   401  	}
   402  	_, err := r.Stat(ctx, rev) // download rev into local repo
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	// r.Stat acquires r.mu, so lock after that.
   408  	unlock, err := r.mu.Lock()
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	defer unlock()
   413  
   414  	out, err := Run(ctx, r.dir, r.cmd.readFile(rev, file, r.remote))
   415  	if err != nil {
   416  		return nil, fs.ErrNotExist
   417  	}
   418  	return out, nil
   419  }
   420  
   421  func (r *vcsRepo) RecentTag(ctx context.Context, rev, prefix string, allowed func(string) bool) (tag string, err error) {
   422  	// We don't technically need to lock here since we're returning an error
   423  	// uncondititonally, but doing so anyway will help to avoid baking in
   424  	// lock-inversion bugs.
   425  	unlock, err := r.mu.Lock()
   426  	if err != nil {
   427  		return "", err
   428  	}
   429  	defer unlock()
   430  
   431  	return "", vcsErrorf("vcs %s: RecentTag: %w", r.cmd.vcs, errors.ErrUnsupported)
   432  }
   433  
   434  func (r *vcsRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, error) {
   435  	unlock, err := r.mu.Lock()
   436  	if err != nil {
   437  		return false, err
   438  	}
   439  	defer unlock()
   440  
   441  	return false, vcsErrorf("vcs %s: DescendsFrom: %w", r.cmd.vcs, errors.ErrUnsupported)
   442  }
   443  
   444  func (r *vcsRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) {
   445  	if r.cmd.readZip == nil && r.cmd.doReadZip == nil {
   446  		return nil, vcsErrorf("vcs %s: ReadZip: %w", r.cmd.vcs, errors.ErrUnsupported)
   447  	}
   448  
   449  	unlock, err := r.mu.Lock()
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	defer unlock()
   454  
   455  	if rev == "latest" {
   456  		rev = r.cmd.latest
   457  	}
   458  	f, err := os.CreateTemp("", "go-readzip-*.zip")
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  	if r.cmd.doReadZip != nil {
   463  		lw := &limitedWriter{
   464  			W:               f,
   465  			N:               maxSize,
   466  			ErrLimitReached: errors.New("ReadZip: encoded file exceeds allowed size"),
   467  		}
   468  		err = r.cmd.doReadZip(ctx, lw, r.dir, rev, subdir, r.remote)
   469  		if err == nil {
   470  			_, err = f.Seek(0, io.SeekStart)
   471  		}
   472  	} else if r.cmd.vcs == "fossil" {
   473  		// If you run
   474  		//	fossil zip -R .fossil --name prefix trunk /tmp/x.zip
   475  		// fossil fails with "unable to create directory /tmp" [sic].
   476  		// Change the command to run in /tmp instead,
   477  		// replacing the -R argument with an absolute path.
   478  		args := r.cmd.readZip(rev, subdir, r.remote, filepath.Base(f.Name()))
   479  		for i := range args {
   480  			if args[i] == ".fossil" {
   481  				args[i] = filepath.Join(r.dir, ".fossil")
   482  			}
   483  		}
   484  		_, err = Run(ctx, filepath.Dir(f.Name()), args)
   485  	} else {
   486  		_, err = Run(ctx, r.dir, r.cmd.readZip(rev, subdir, r.remote, f.Name()))
   487  	}
   488  	if err != nil {
   489  		f.Close()
   490  		os.Remove(f.Name())
   491  		return nil, err
   492  	}
   493  	return &deleteCloser{f}, nil
   494  }
   495  
   496  // deleteCloser is a file that gets deleted on Close.
   497  type deleteCloser struct {
   498  	*os.File
   499  }
   500  
   501  func (d *deleteCloser) Close() error {
   502  	defer os.Remove(d.File.Name())
   503  	return d.File.Close()
   504  }
   505  
   506  func hgParseStat(rev, out string) (*RevInfo, error) {
   507  	f := strings.Fields(out)
   508  	if len(f) < 3 {
   509  		return nil, vcsErrorf("unexpected response from hg log: %q", out)
   510  	}
   511  	hash := f[0]
   512  	version := rev
   513  	if strings.HasPrefix(hash, version) {
   514  		version = hash // extend to full hash
   515  	}
   516  	t, err := strconv.ParseInt(f[1], 10, 64)
   517  	if err != nil {
   518  		return nil, vcsErrorf("invalid time from hg log: %q", out)
   519  	}
   520  
   521  	var tags []string
   522  	for _, tag := range f[3:] {
   523  		if tag != "tip" {
   524  			tags = append(tags, tag)
   525  		}
   526  	}
   527  	sort.Strings(tags)
   528  
   529  	info := &RevInfo{
   530  		Origin: &Origin{
   531  			Hash: hash,
   532  		},
   533  		Name:    hash,
   534  		Short:   ShortenSHA1(hash),
   535  		Time:    time.Unix(t, 0).UTC(),
   536  		Version: version,
   537  		Tags:    tags,
   538  	}
   539  	return info, nil
   540  }
   541  
   542  func bzrParseStat(rev, out string) (*RevInfo, error) {
   543  	var revno int64
   544  	var tm time.Time
   545  	for _, line := range strings.Split(out, "\n") {
   546  		if line == "" || line[0] == ' ' || line[0] == '\t' {
   547  			// End of header, start of commit message.
   548  			break
   549  		}
   550  		if line[0] == '-' {
   551  			continue
   552  		}
   553  		before, after, found := strings.Cut(line, ":")
   554  		if !found {
   555  			// End of header, start of commit message.
   556  			break
   557  		}
   558  		key, val := before, strings.TrimSpace(after)
   559  		switch key {
   560  		case "revno":
   561  			if j := strings.Index(val, " "); j >= 0 {
   562  				val = val[:j]
   563  			}
   564  			i, err := strconv.ParseInt(val, 10, 64)
   565  			if err != nil {
   566  				return nil, vcsErrorf("unexpected revno from bzr log: %q", line)
   567  			}
   568  			revno = i
   569  		case "timestamp":
   570  			j := strings.Index(val, " ")
   571  			if j < 0 {
   572  				return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
   573  			}
   574  			t, err := time.Parse("2006-01-02 15:04:05 -0700", val[j+1:])
   575  			if err != nil {
   576  				return nil, vcsErrorf("unexpected timestamp from bzr log: %q", line)
   577  			}
   578  			tm = t.UTC()
   579  		}
   580  	}
   581  	if revno == 0 || tm.IsZero() {
   582  		return nil, vcsErrorf("unexpected response from bzr log: %q", out)
   583  	}
   584  
   585  	info := &RevInfo{
   586  		Name:    strconv.FormatInt(revno, 10),
   587  		Short:   fmt.Sprintf("%012d", revno),
   588  		Time:    tm,
   589  		Version: rev,
   590  	}
   591  	return info, nil
   592  }
   593  
   594  func fossilParseStat(rev, out string) (*RevInfo, error) {
   595  	for _, line := range strings.Split(out, "\n") {
   596  		if strings.HasPrefix(line, "uuid:") || strings.HasPrefix(line, "hash:") {
   597  			f := strings.Fields(line)
   598  			if len(f) != 5 || len(f[1]) != 40 || f[4] != "UTC" {
   599  				return nil, vcsErrorf("unexpected response from fossil info: %q", line)
   600  			}
   601  			t, err := time.Parse(time.DateTime, f[2]+" "+f[3])
   602  			if err != nil {
   603  				return nil, vcsErrorf("unexpected response from fossil info: %q", line)
   604  			}
   605  			hash := f[1]
   606  			version := rev
   607  			if strings.HasPrefix(hash, version) {
   608  				version = hash // extend to full hash
   609  			}
   610  			info := &RevInfo{
   611  				Origin: &Origin{
   612  					Hash: hash,
   613  				},
   614  				Name:    hash,
   615  				Short:   ShortenSHA1(hash),
   616  				Time:    t,
   617  				Version: version,
   618  			}
   619  			return info, nil
   620  		}
   621  	}
   622  	return nil, vcsErrorf("unexpected response from fossil info: %q", out)
   623  }
   624  
   625  type limitedWriter struct {
   626  	W               io.Writer
   627  	N               int64
   628  	ErrLimitReached error
   629  }
   630  
   631  func (l *limitedWriter) Write(p []byte) (n int, err error) {
   632  	if l.N > 0 {
   633  		max := len(p)
   634  		if l.N < int64(max) {
   635  			max = int(l.N)
   636  		}
   637  		n, err = l.W.Write(p[:max])
   638  		l.N -= int64(n)
   639  		if err != nil || n >= len(p) {
   640  			return n, err
   641  		}
   642  	}
   643  
   644  	return n, l.ErrLimitReached
   645  }