<commit_before>// Package ftp implements a FTP client as described in RFC 959.
//
// A textproto.Error is returned for errors at the protocol level.
package ftp

import (
	"bufio"
	"context"
	"crypto/tls"
	"errors"
	"io"
	"net"
	"net/textproto"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-multierror"
)

// EntryType describes the different types of an Entry.
type EntryType int

// The differents types of an Entry
const (
	EntryTypeFile EntryType = iota
	EntryTypeFolder
	EntryTypeLink
)

// TransferType denotes the formats for transfering Entries.
type TransferType string

// The different transfer types
const (
	TransferTypeBinary = "I"
	TransferTypeASCII  = "A"
)

// Time format used by the MDTM and MFMT commands
const timeFormat = "20060102150405"

// ServerConn represents the connection to a remote FTP server.
// A single connection only supports one in-flight data connection.
// It is not safe to be called concurrently.
type ServerConn struct {
	options *dialOptions
	conn    *textproto.Conn // connection wrapper for text protocol
	netConn net.Conn        // underlying network connection
	host    string

	// Server capabilities discovered at runtime
	features      map[string]string
	skipEPSV      bool
	mlstSupported bool
	mfmtSupported bool
	mdtmSupported bool
	mdtmCanWrite  bool
	usePRET       bool
}

// DialOption represents an option to start a new connection with Dial
type DialOption struct {
	setup func(do *dialOptions)
}

// dialOptions contains all the options set by DialOption.setup
type dialOptions struct {
	context     context.Context
	dialer      net.Dialer
	tlsConfig   *tls.Config
	explicitTLS bool
	conn        net.Conn
	disableEPSV bool
	disableUTF8 bool
	disableMLSD bool
	writingMDTM bool
	location    *time.Location
	debugOutput io.Writer
	dialFunc    func(network, address string) (net.Conn, error)
	shutTimeout time.Duration // time to wait for data connection closing status
}

// Entry describes a file and is returned by List().
type Entry struct {
	Name   string
	Target string // target of symbolic link
	Type   EntryType
	Size   uint64
	Time   time.Time
}

// Response represents a data-connection
type Response struct {
	conn   net.Conn
	c      *ServerConn
	closed bool
}

// Dial connects to the specified address with optional options
func Dial(addr string, options ...DialOption) (*ServerConn, error) {
	do := &dialOptions{}
	for _, option := range options {
		option.setup(do)
	}

	if do.location == nil {
		do.location = time.UTC
	}

	tconn := do.conn
	if tconn == nil {
		var err error

		if do.dialFunc != nil {
			tconn, err = do.dialFunc("tcp", addr)
		} else if do.tlsConfig != nil && !do.explicitTLS {
			tconn, err = tls.DialWithDialer(&do.dialer, "tcp", addr, do.tlsConfig)
		} else {
			ctx := do.context

			if ctx == nil {
				ctx = context.Background()
			}

			tconn, err = do.dialer.DialContext(ctx, "tcp", addr)
		}

		if err != nil {
			return nil, err
		}
	}

	// Use the resolved IP address in case addr contains a domain name
	// If we use the domain name, we might not resolve to the same IP.
	remoteAddr := tconn.RemoteAddr().(*net.TCPAddr)

	c := &ServerConn{
		options:  do,
		features: make(map[string]string),
		conn:     textproto.NewConn(do.wrapConn(tconn)),
		netConn:  tconn,
		host:     remoteAddr.IP.String(),
	}

	_, _, err := c.conn.ReadResponse(StatusReady)
	if err != nil {
		_ = c.Quit()
		return nil, err
	}

	if do.explicitTLS {
		if err := c.authTLS(); err != nil {
			_ = c.Quit()
			return nil, err
		}
		tconn = tls.Client(tconn, do.tlsConfig)
		c.conn = textproto.NewConn(do.wrapConn(tconn))
	}

	return c, nil
}

// DialWithTimeout returns a DialOption that configures the ServerConn with specified timeout
func DialWithTimeout(timeout time.Duration) DialOption {
	return DialOption{func(do *dialOptions) {
		do.dialer.Timeout = timeout
	}}
}

// DialWithShutTimeout returns a DialOption that configures the ServerConn with
// maximum time to wait for the data closing status on control connection
// and nudging the control connection deadline before reading status.
func DialWithShutTimeout(shutTimeout time.Duration) DialOption {
	return DialOption{func(do *dialOptions) {
		do.shutTimeout = shutTimeout
	}}
}

// DialWithDialer returns a DialOption that configures the ServerConn with specified net.Dialer
func DialWithDialer(dialer net.Dialer) DialOption {
	return DialOption{func(do *dialOptions) {
		do.dialer = dialer
	}}
}

// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
func DialWithNetConn(conn net.Conn) DialOption {
	return DialOption{func(do *dialOptions) {
		do.conn = conn
	}}
}

// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
// Note that EPSV is only used when advertised in the server features.
func DialWithDisabledEPSV(disabled bool) DialOption {
	return DialOption{func(do *dialOptions) {
		do.disableEPSV = disabled
	}}
}

// DialWithDisabledUTF8 returns a DialOption that configures the ServerConn with UTF8 option disabled
func DialWithDisabledUTF8(disabled bool) DialOption {
	return DialOption{func(do *dialOptions) {
		do.disableUTF8 = disabled
	}}
}

// DialWithDisabledMLSD returns a DialOption that configures the ServerConn with MLSD option disabled
//
// This is useful for servers which advertise MLSD (eg some versions
// of Serv-U) but don't support it properly.
func DialWithDisabledMLSD(disabled bool) DialOption {
	return DialOption{func(do *dialOptions) {
		do.disableMLSD = disabled
	}}
}

// DialWithWritingMDTM returns a DialOption making ServerConn use MDTM to set file time<commit_msg>DialWithNetConn is a special case of DialWithDialFunc<commit_after>// Package ftp implements a FTP client as described in RFC 959.
//
// A textproto.Error is returned for errors at the protocol level.
package ftp

import (
	"bufio"
	"context"
	"crypto/tls"
	"errors"
	"io"
	"net"
	"net/textproto"
	"strconv"
	"strings"
	"time"

	"github.com/hashicorp/go-multierror"
)

// EntryType describes the different types of an Entry.
type EntryType int

// The differents types of an Entry
const (
	EntryTypeFile EntryType = iota
	EntryTypeFolder
	EntryTypeLink
)

// TransferType denotes the formats for transfering Entries.
type TransferType string

// The different transfer types
const (
	TransferTypeBinary = "I"
	TransferTypeASCII  = "A"
)

// Time format used by the MDTM and MFMT commands
const timeFormat = "20060102150405"

// ServerConn represents the connection to a remote FTP server.
// A single connection only supports one in-flight data connection.
// It is not safe to be called concurrently.
type ServerConn struct {
	options *dialOptions
	conn    *textproto.Conn // connection wrapper for text protocol
	netConn net.Conn        // underlying network connection
	host    string

	// Server capabilities discovered at runtime
	features      map[string]string
	skipEPSV      bool
	mlstSupported bool
	mfmtSupported bool
	mdtmSupported bool
	mdtmCanWrite  bool
	usePRET       bool
}

// DialOption represents an option to start a new connection with Dial
type DialOption struct {
	setup func(do *dialOptions)
}

// dialOptions contains all the options set by DialOption.setup
type dialOptions struct {
	context     context.Context
	dialer      net.Dialer
	tlsConfig   *tls.Config
	explicitTLS bool
	disableEPSV bool
	disableUTF8 bool
	disableMLSD bool
	writingMDTM bool
	location    *time.Location
	debugOutput io.Writer
	dialFunc    func(network, address string) (net.Conn, error)
	shutTimeout time.Duration // time to wait for data connection closing status
}

// Entry describes a file and is returned by List().
type Entry struct {
	Name   string
	Target string // target of symbolic link
	Type   EntryType
	Size   uint64
	Time   time.Time
}

// Response represents a data-connection
type Response struct {
	conn   net.Conn
	c      *ServerConn
	closed bool
}

// Dial connects to the specified address with optional options
func Dial(addr string, options ...DialOption) (*ServerConn, error) {
	do := &dialOptions{}
	for _, option := range options {
		option.setup(do)
	}

	if do.location == nil {
		do.location = time.UTC
	}

	dialFunc := do.dialFunc

	if dialFunc == nil {
		if do.tlsConfig != nil && !do.explicitTLS {
			dialFunc = func(network, address string) (net.Conn, error) {
				return tls.DialWithDialer(&do.dialer, network, addr, do.tlsConfig)
			}
		} else {
			ctx := do.context

			if ctx == nil {
				ctx = context.Background()
			}

			dialFunc = func(network, address string) (net.Conn, error) {
				return do.dialer.DialContext(ctx, network, addr)
			}
		}
	}

	tconn, err := dialFunc("tcp", addr)
	if err != nil {
		return nil, err
	}

	// Use the resolved IP address in case addr contains a domain name
	// If we use the domain name, we might not resolve to the same IP.
	remoteAddr := tconn.RemoteAddr().(*net.TCPAddr)

	c := &ServerConn{
		options:  do,
		features: make(map[string]string),
		conn:     textproto.NewConn(do.wrapConn(tconn)),
		netConn:  tconn,
		host:     remoteAddr.IP.String(),
	}

	_, _, err = c.conn.ReadResponse(StatusReady)
	if err != nil {
		_ = c.Quit()
		return nil, err
	}

	if do.explicitTLS {
		if err := c.authTLS(); err != nil {
			_ = c.Quit()
			return nil, err
		}
		tconn = tls.Client(tconn, do.tlsConfig)
		c.conn = textproto.NewConn(do.wrapConn(tconn))
	}

	return c, nil
}

// DialWithTimeout returns a DialOption that configures the ServerConn with specified timeout
func DialWithTimeout(timeout time.Duration) DialOption {
	return DialOption{func(do *dialOptions) {
		do.dialer.Timeout = timeout
	}}
}

// DialWithShutTimeout returns a DialOption that configures the ServerConn with
// maximum time to wait for the data closing status on control connection
// and nudging the control connection deadline before reading status.
func DialWithShutTimeout(shutTimeout time.Duration) DialOption {
	return DialOption{func(do *dialOptions) {
		do.shutTimeout = shutTimeout
	}}
}

// DialWithDialer returns a DialOption that configures the ServerConn with specified net.Dialer
func DialWithDialer(dialer net.Dialer) DialOption {
	return DialOption{func(do *dialOptions) {
		do.dialer = dialer
	}}
}

// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
//
// Deprecated: Use [DialWithDialFunc] instead
func DialWithNetConn(conn net.Conn) DialOption {
	return DialWithDialFunc(func(network, address string) (net.Conn, error) {
		return conn, nil
	})
}

// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
// Note that EPSV is only used when advertised in the server features.
func DialWithDisabledEPSV(disabled bool) DialOption {
	return DialOption{func(do *dialOptions) {
		do.disableEPSV = disabled
	}}
}

// DialWithDisabledUTF8 returns a DialOption that configures the ServerConn with UTF8 option disabled
func DialWithDisabledUTF8(disabled bool) DialOption {
	return DialOption{func(do *dialOptions) {
		do.disableUTF8 = disabled
	}}
}

// DialWithDisabledMLSD returns a DialOption that configures the ServerConn with MLSD option disabled
//
// This is useful for servers which advertise MLSD (eg some versions
// of Serv-U) but don't support it properly.
func DialWithDisabledMLSD(disabled bool) DialOption {
	return DialOption{func(do *dialOptions) {
		do.disableMLSD = disabled
	}}
}

// DialWithWritingMDTM returns a DialOption making ServerConn use MDTM to set file time
{'fraction_non_alphanumeric': 0.08352203448876502, 'fraction_numerical': 0.004180456366486675, 'mean_word_length': 3.7845833333333334, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2899958', 'n_tokens_mistral': 3799, 'n_tokens_neox': 3568, 'n_words': 1222}