First parts of new FTP LIST response parsing code.

Added parser for "ls" listing style, and tests. This is not yet used by the browser (will do that in a following CL).

TEST=Covered by net_unittests.
BUG=25520

Review URL: http://codereview.chromium.org/244008

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@30354 0039d316-1c4b-4281-b951-d872f2087c98
parent 792785dd
...@@ -174,6 +174,8 @@ TrimPositions TrimWhitespace(const std::string& input, ...@@ -174,6 +174,8 @@ TrimPositions TrimWhitespace(const std::string& input,
// (3) All other whitespace sequences are converted to single spaces. // (3) All other whitespace sequences are converted to single spaces.
std::wstring CollapseWhitespace(const std::wstring& text, std::wstring CollapseWhitespace(const std::wstring& text,
bool trim_sequences_with_line_breaks); bool trim_sequences_with_line_breaks);
string16 CollapseWhitespace(const string16& text,
bool trim_sequences_with_line_breaks);
std::string CollapseWhitespaceASCII(const std::string& text, std::string CollapseWhitespaceASCII(const std::string& text,
bool trim_sequences_with_line_breaks); bool trim_sequences_with_line_breaks);
......
...@@ -279,6 +279,12 @@ NET_ERROR(NETWORK_IO_SUSPENDED, -331) ...@@ -279,6 +279,12 @@ NET_ERROR(NETWORK_IO_SUSPENDED, -331)
// FLIP data received without receiving a SYN_REPLY on the stream. // FLIP data received without receiving a SYN_REPLY on the stream.
NET_ERROR(SYN_REPLY_NOT_RECEIVED, -332) NET_ERROR(SYN_REPLY_NOT_RECEIVED, -332)
// Converting the response to target encoding failed.
NET_ERROR(ENCODING_CONVERSION_FAILED, -333)
// The server sent an FTP directory listing in a format we do not understand.
NET_ERROR(UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT, -334)
// The cache does not have the requested entry. // The cache does not have the requested entry.
NET_ERROR(CACHE_MISS, -400) NET_ERROR(CACHE_MISS, -400)
......
drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 .
drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 ..
-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 .message
-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README
-rw-r--r-- 1 ftp ftp 560 Sep 28 2007 index.html
drwxr-xr-x 33 ftp ftp 4096 Aug 12 2008 pub
d
.
current
5
15
18
11
d
..
current
5
15
18
11
-
.message
2007
11
1
0
0
-
README
2007
11
1
0
0
-
index.html
2007
9
28
0
0
d
pub
2008
8
12
0
0
drwxr-xr-x 3 0 0 4096 Sep 18 2008 .
drwxr-xr-x 3 0 0 4096 Sep 18 2008 ..
lrwxrwxrwx 1 0 509 1 Nov 08 2007 ftp -> .
lrwxrwxrwx 1 0 0 3 Oct 12 2007 mirror -> pub
lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub
lrwxrwxrwx 1 0 0 27 Oct 16 2007 site -> vol/1/.CLUSTER/var_ftp/site
drwxr-xr-x 7 0 0 4096 Jul 02 2008 vol
d
.
2008
9
18
0
0
d
..
2008
9
18
0
0
l
ftp
2007
11
8
0
0
l
mirror
2007
10
12
0
0
l
pub
2008
9
18
0
0
l
site
2007
10
16
0
0
d
vol
2008
7
2
0
0
// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this
// source code is governed by a BSD-style license that can be found in the
// LICENSE file.
#include "net/ftp/ftp_directory_listing_buffer.h"
#include "base/i18n/icu_string_conversions.h"
#include "base/stl_util-inl.h"
#include "base/string_util.h"
#include "net/base/net_errors.h"
#include "net/ftp/ftp_directory_listing_parsers.h"
#include "unicode/ucsdet.h"
namespace {
// A very simple-minded character encoding detection.
// TODO(jungshik): We can apply more heuristics here (e.g. using various hints
// like TLD, the UI language/default encoding of a client, etc). In that case,
// this should be pulled out of here and moved somewhere in base because there
// can be other use cases.
std::string DetectEncoding(const std::string& text) {
if (IsStringASCII(text))
return std::string();
UErrorCode status = U_ZERO_ERROR;
UCharsetDetector* detector = ucsdet_open(&status);
ucsdet_setText(detector, text.data(), static_cast<int32_t>(text.length()),
&status);
const UCharsetMatch* match = ucsdet_detect(detector, &status);
const char* encoding = ucsdet_getName(match, &status);
// Should we check the quality of the match? A rather arbitrary number is
// assigned by ICU and it's hard to come up with a lower limit.
if (U_FAILURE(status))
return std::string();
return encoding;
}
} // namespace
namespace net {
FtpDirectoryListingBuffer::FtpDirectoryListingBuffer()
: current_parser_(NULL) {
parsers_.insert(new FtpLsDirectoryListingParser());
}
FtpDirectoryListingBuffer::~FtpDirectoryListingBuffer() {
STLDeleteElements(&parsers_);
}
int FtpDirectoryListingBuffer::ConsumeData(const char* data, int data_length) {
buffer_.append(data, data_length);
if (!encoding_.empty() || buffer_.length() > 1024) {
int rv = ExtractFullLinesFromBuffer();
if (rv != OK)
return rv;
}
return ParseLines();
}
int FtpDirectoryListingBuffer::ProcessRemainingData() {
int rv = ExtractFullLinesFromBuffer();
if (rv != OK)
return rv;
return ParseLines();
}
bool FtpDirectoryListingBuffer::EntryAvailable() const {
return (current_parser_ ? current_parser_->EntryAvailable() : false);
}
FtpDirectoryListingEntry FtpDirectoryListingBuffer::PopEntry() {
DCHECK(EntryAvailable());
return current_parser_->PopEntry();
}
bool FtpDirectoryListingBuffer::ConvertToDetectedEncoding(
const std::string& from, string16* to) {
std::string encoding(encoding_.empty() ? "ascii" : encoding_);
return base::CodepageToUTF16(from, encoding.c_str(),
base::OnStringConversionError::FAIL, to);
}
int FtpDirectoryListingBuffer::ExtractFullLinesFromBuffer() {
if (encoding_.empty())
encoding_ = DetectEncoding(buffer_);
int cut_pos = 0;
for (size_t i = 0; i < buffer_.length(); ++i) {
if (i >= 1 && buffer_[i - 1] == '\r' && buffer_[i] == '\n') {
std::string line(buffer_.substr(cut_pos, i - cut_pos - 1));
cut_pos = i + 1;
string16 line_converted;
if (!ConvertToDetectedEncoding(line, &line_converted)) {
buffer_.erase(0, cut_pos);
return ERR_ENCODING_CONVERSION_FAILED;
}
lines_.push_back(line_converted);
}
}
buffer_.erase(0, cut_pos);
return OK;
}
int FtpDirectoryListingBuffer::ParseLines() {
while (!lines_.empty()) {
string16 line = lines_.front();
lines_.pop_front();
if (current_parser_) {
if (!current_parser_->ConsumeLine(line))
return ERR_FAILED;
} else {
ParserSet::iterator i = parsers_.begin();
while (i != parsers_.end()) {
if ((*i)->ConsumeLine(line)) {
i++;
} else {
delete *i;
parsers_.erase(i++);
}
}
if (parsers_.empty())
return ERR_UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT;
if (parsers_.size() == 1)
current_parser_ = *parsers_.begin();
}
}
return OK;
}
} // namespace net
// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this
// source code is governed by a BSD-style license that can be found in the
// LICENSE file.
#ifndef NET_FTP_FTP_DIRECTORY_LISTING_BUFFER_H_
#define NET_FTP_FTP_DIRECTORY_LISTING_BUFFER_H_
#include <deque>
#include <set>
#include <string>
#include "base/basictypes.h"
#include "base/string16.h"
#include "base/time.h"
namespace net {
struct FtpDirectoryListingEntry;
class FtpDirectoryListingParser;
class FtpDirectoryListingBuffer {
public:
FtpDirectoryListingBuffer();
~FtpDirectoryListingBuffer();
// Called when data is received from the data socket. Returns network
// error code.
int ConsumeData(const char* data, int data_length);
// Called when all received data has been consumed by this buffer. Tells the
// buffer to try to parse remaining raw data and returns network error code.
int ProcessRemainingData();
bool EntryAvailable() const;
// Returns the next entry. It is an error to call this function
// unless EntryAvailable returns true.
FtpDirectoryListingEntry PopEntry();
private:
typedef std::set<FtpDirectoryListingParser*> ParserSet;
// Converts the string |from| to detected encoding and stores it in |to|.
// Returns true on success.
bool ConvertToDetectedEncoding(const std::string& from, string16* to);
// Tries to extract full lines from the raw buffer, converting them to the
// detected encoding. Returns network error code.
int ExtractFullLinesFromBuffer();
// Tries to parse full lines stored in |lines_|. Returns network error code.
int ParseLines();
// Detected encoding of the response (empty if unknown or ASCII).
std::string encoding_;
// Buffer to keep not-yet-split data.
std::string buffer_;
// CRLF-delimited lines, without the CRLF, not yet consumed by parser.
std::deque<string16> lines_;
// A collection of parsers for different listing styles. The parsers are owned
// by this FtpDirectoryListingBuffer.
ParserSet parsers_;
// When we're sure about the listing format, its parser is stored in
// |current_parser_|.
FtpDirectoryListingParser* current_parser_;
DISALLOW_COPY_AND_ASSIGN(FtpDirectoryListingBuffer);
};
} // namespace net
#endif // NET_FTP_FTP_DIRECTORY_LISTING_BUFFER_H_
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/ftp/ftp_directory_listing_buffer.h"
#include "base/file_util.h"
#include "base/path_service.h"
#include "base/string_tokenizer.h"
#include "base/string_util.h"
#include "net/base/net_errors.h"
#include "net/ftp/ftp_directory_listing_parsers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
TEST(FtpDirectoryListingBufferTest, Parse) {
const char* test_files[] = {
"dir-listing-ls-1",
"dir-listing-ls-2",
};
FilePath test_dir;
PathService::Get(base::DIR_SOURCE_ROOT, &test_dir);
test_dir = test_dir.AppendASCII("net");
test_dir = test_dir.AppendASCII("data");
test_dir = test_dir.AppendASCII("ftp");
for (size_t i = 0; i < arraysize(test_files); i++) {
SCOPED_TRACE(StringPrintf("Test[%d]: %s", i, test_files[i]));
net::FtpDirectoryListingBuffer buffer;
std::string test_listing;
EXPECT_TRUE(file_util::ReadFileToString(test_dir.AppendASCII(test_files[i]),
&test_listing));
EXPECT_EQ(net::OK, buffer.ConsumeData(test_listing.data(),
test_listing.length()));
EXPECT_EQ(net::OK, buffer.ProcessRemainingData());
std::string expected_listing;
ASSERT_TRUE(file_util::ReadFileToString(
test_dir.AppendASCII(std::string(test_files[i]) + ".expected"),
&expected_listing));
std::vector<std::string> lines;
StringTokenizer tokenizer(expected_listing, "\r\n");
while (tokenizer.GetNext())
lines.push_back(tokenizer.token());
ASSERT_EQ(0U, lines.size() % 7);
for (size_t i = 0; i < lines.size() / 7; i++) {
std::string type(lines[7 * i]);
std::string name(lines[7 * i + 1]);
SCOPED_TRACE(StringPrintf("Filename: %s", name.c_str()));
int year;
if (lines[7 * i + 2] == "current") {
base::Time::Exploded now_exploded;
base::Time::Now().LocalExplode(&now_exploded);
year = now_exploded.year;
} else {
year = StringToInt(lines[7 * i + 2]);
}
int month = StringToInt(lines[7 * i + 3]);
int day_of_month = StringToInt(lines[7 * i + 4]);
int hour = StringToInt(lines[7 * i + 5]);
int minute = StringToInt(lines[7 * i + 6]);
ASSERT_TRUE(buffer.EntryAvailable());
net::FtpDirectoryListingEntry entry = buffer.PopEntry();
if (type == "d") {
EXPECT_EQ(net::FtpDirectoryListingEntry::DIRECTORY, entry.type);
} else if (type == "-") {
EXPECT_EQ(net::FtpDirectoryListingEntry::FILE, entry.type);
} else if (type == "l") {
EXPECT_EQ(net::FtpDirectoryListingEntry::SYMLINK, entry.type);
} else {
ADD_FAILURE() << "invalid gold test data: " << type;
}
EXPECT_EQ(UTF8ToUTF16(name), entry.name);
base::Time::Exploded time_exploded;
entry.last_modified.LocalExplode(&time_exploded);
EXPECT_EQ(year, time_exploded.year);
EXPECT_EQ(month, time_exploded.month);
EXPECT_EQ(day_of_month, time_exploded.day_of_month);
EXPECT_EQ(hour, time_exploded.hour);
EXPECT_EQ(minute, time_exploded.minute);
EXPECT_EQ(0, time_exploded.second);
EXPECT_EQ(0, time_exploded.millisecond);
}
EXPECT_FALSE(buffer.EntryAvailable());
}
}
} // namespace
// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this
// source code is governed by a BSD-style license that can be found in the
// LICENSE file.
#include "net/ftp/ftp_directory_listing_parsers.h"
#include "base/string_util.h"
namespace {
bool LooksLikeUnixPermission(const string16& text) {
if (text.length() != 3)
return false;
return ((text[0] == 'r' || text[0] == '-') &&
(text[1] == 'w' || text[1] == '-') &&
(text[2] == 'x' || text[2] == 's' || text[2] == 'S' ||
text[2] == '-'));
}
bool LooksLikeUnixPermissionsListing(const string16& text) {
if (text.length() != 10)
return false;
if (text[0] != 'b' && text[0] != 'c' && text[0] != 'd' &&
text[0] != 'l' && text[0] != 'p' && text[0] != 's' &&
text[0] != '-')
return false;
return (LooksLikeUnixPermission(text.substr(1, 3)) &&
LooksLikeUnixPermission(text.substr(4, 3)) &&
LooksLikeUnixPermission(text.substr(7, 3)));
}
bool IsStringNonNegativeNumber(const string16& text) {
int number;
if (!StringToInt(text, &number))
return false;
return number >= 0;
}
bool ThreeLetterMonthToNumber(const string16& text, int* number) {
const static char* months[] = { "jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec" };
for (size_t i = 0; i < arraysize(months); i++) {
if (LowerCaseEqualsASCII(text, months[i])) {
*number = i + 1;
return true;
}
}
return false;
}
bool UnixDateListingToTime(const std::vector<string16>& columns,
base::Time* time) {
DCHECK_EQ(9U, columns.size());
base::Time::Exploded time_exploded = { 0 };
if (!ThreeLetterMonthToNumber(columns[5], &time_exploded.month))
return false;
if (!StringToInt(columns[6], &time_exploded.day_of_month))
return false;
if (!StringToInt(columns[7], &time_exploded.year)) {
// Maybe it's time. Does it look like time (MM:HH)?
if (columns[7].length() != 5 || columns[7][2] != ':')
return false;
if (!StringToInt(columns[7].substr(0, 2), &time_exploded.hour))
return false;
if (!StringToInt(columns[7].substr(3, 2), &time_exploded.minute))
return false;
// Use current year.
base::Time::Exploded now_exploded;
base::Time::Now().LocalExplode(&now_exploded);
time_exploded.year = now_exploded.year;
}
// We don't know the time zone of the server, so just use local time.
*time = base::Time::FromLocalExploded(time_exploded);
return true;
}
} // namespace
namespace net {
FtpDirectoryListingParser::~FtpDirectoryListingParser() {
}
FtpLsDirectoryListingParser::FtpLsDirectoryListingParser() {
}
bool FtpLsDirectoryListingParser::ConsumeLine(const string16& line) {
std::vector<string16> columns;
SplitString(CollapseWhitespace(line, false), ' ', &columns);
if (columns.size() == 11) {
// Check if it is a symlink.
if (columns[9] != ASCIIToUTF16("->"))
return false;
// Drop the symlink target from columns, we don't use it.
columns.resize(9);
}
if (columns.size() != 9)
return false;
if (!LooksLikeUnixPermissionsListing(columns[0]))
return false;
FtpDirectoryListingEntry entry;
if (columns[0][0] == 'l') {
entry.type = FtpDirectoryListingEntry::SYMLINK;
} else if (columns[0][0] == 'd') {
entry.type = FtpDirectoryListingEntry::DIRECTORY;
} else {
entry.type = FtpDirectoryListingEntry::FILE;
}
if (!IsStringNonNegativeNumber(columns[1]))
return false;
if (!IsStringNonNegativeNumber(columns[4]))
return false;
if (!UnixDateListingToTime(columns, &entry.last_modified))
return false;
entry.name = columns[8];
entries_.push(entry);
return true;
}
bool FtpLsDirectoryListingParser::EntryAvailable() const {
return !entries_.empty();
}
FtpDirectoryListingEntry FtpLsDirectoryListingParser::PopEntry() {
FtpDirectoryListingEntry entry = entries_.front();
entries_.pop();
return entry;
}
} // namespace net
// Copyright (c) 2009 The Chromium Authors. All rights reserved. Use of this
// source code is governed by a BSD-style license that can be found in the
// LICENSE file.
#ifndef NET_FTP_FTP_DIRECTORY_LISTING_PARSERS_H_
#define NET_FTP_FTP_DIRECTORY_LISTING_PARSERS_H_
#include <queue>
#include "base/basictypes.h"
#include "base/string16.h"
#include "base/time.h"
#include "net/ftp/ftp_server_type_histograms.h"
namespace net {
struct FtpDirectoryListingEntry {
enum Type {
FILE,
DIRECTORY,
SYMLINK,
};
Type type;
string16 name;
// Last modified time, in local time zone.
base::Time last_modified;
};
class FtpDirectoryListingParser {
public:
virtual ~FtpDirectoryListingParser();
// Adds |line| to the internal parsing buffer. Returns true on success.
virtual bool ConsumeLine(const string16& line) = 0;
// Returns true if there is at least one FtpDirectoryListingEntry available.
virtual bool EntryAvailable() const = 0;
// Returns the next entry. It is an error to call this function unless
// EntryAvailable returns true.
virtual FtpDirectoryListingEntry PopEntry() = 0;
};
class FtpLsDirectoryListingParser : public FtpDirectoryListingParser {
public:
FtpLsDirectoryListingParser();
// FtpDirectoryListingParser methods:
virtual bool ConsumeLine(const string16& line);
virtual bool EntryAvailable() const;
virtual FtpDirectoryListingEntry PopEntry();
private:
std::queue<FtpDirectoryListingEntry> entries_;
DISALLOW_COPY_AND_ASSIGN(FtpLsDirectoryListingParser);
};
} // namespace net
#endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSERS_H_
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/ftp/ftp_directory_listing_parsers.h"
#include "base/string_util.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
struct SingleLineTestData {
const char* input;
net::FtpDirectoryListingEntry::Type type;
const char* filename;
int year;
int month;
int day_of_month;
int hour;
int minute;
};
class FtpDirectoryListingParsersTest : public testing::Test {
protected:
FtpDirectoryListingParsersTest() {
}
void RunSingleLineTestCase(net::FtpDirectoryListingParser* parser,
const SingleLineTestData& test_case) {
ASSERT_TRUE(parser->ConsumeLine(UTF8ToUTF16(test_case.input)));
ASSERT_TRUE(parser->EntryAvailable());
net::FtpDirectoryListingEntry entry = parser->PopEntry();
EXPECT_EQ(test_case.type, entry.type);
EXPECT_EQ(UTF8ToUTF16(test_case.filename), entry.name);
base::Time::Exploded time_exploded;
entry.last_modified.LocalExplode(&time_exploded);
EXPECT_EQ(test_case.year, time_exploded.year);
EXPECT_EQ(test_case.month, time_exploded.month);
EXPECT_EQ(test_case.day_of_month, time_exploded.day_of_month);
EXPECT_EQ(test_case.hour, time_exploded.hour);
EXPECT_EQ(test_case.minute, time_exploded.minute);
EXPECT_EQ(0, time_exploded.second);
EXPECT_EQ(0, time_exploded.millisecond);
}
private:
DISALLOW_COPY_AND_ASSIGN(FtpDirectoryListingParsersTest);
};
TEST_F(FtpDirectoryListingParsersTest, Ls) {
base::Time::Exploded now_exploded;
base::Time::Now().LocalExplode(&now_exploded);
const struct SingleLineTestData good_cases[] = {
{ "-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README",
net::FtpDirectoryListingEntry::FILE, "README",
2007, 11, 1, 0, 0 },
{ "drwxr-xr-x 3 ftp ftp 4096 May 15 18:11 directory",
net::FtpDirectoryListingEntry::DIRECTORY, "directory",
now_exploded.year, 5, 15, 18, 11 },
{ "lrwxrwxrwx 1 0 0 26 Sep 18 2008 pub -> vol/1/.CLUSTER/var_ftp/pub",
net::FtpDirectoryListingEntry::SYMLINK, "pub",
2008, 9, 18, 0, 0 },
{ "lrwxrwxrwx 1 0 0 3 Oct 12 13:37 mirror -> pub",
net::FtpDirectoryListingEntry::SYMLINK, "mirror",
now_exploded.year, 10, 12, 13, 37 },
};
for (size_t i = 0; i < arraysize(good_cases); i++) {
SCOPED_TRACE(StringPrintf("Test[%d]: %s", i, good_cases[i]));
net::FtpLsDirectoryListingParser parser;
RunSingleLineTestCase(&parser, good_cases[i]);
}
const char* bad_cases[] = {
"",
"garbage",
"-rw-r--r-- 1 ftp ftp",
"-rw-r--rgb 1 ftp ftp 528 Nov 01 2007 README",
"-rw-rgbr-- 1 ftp ftp 528 Nov 01 2007 README",
"qrwwr--r-- 1 ftp ftp 528 Nov 01 2007 README",
"-rw-r--r-- -1 ftp ftp 528 Nov 01 2007 README",
"-rw-r--r-- 1 ftp ftp -528 Nov 01 2007 README",
"-rw-r--r-- 1 ftp ftp 528 Foo 01 2007 README",
};
for (size_t i = 0; i < arraysize(bad_cases); i++) {
net::FtpLsDirectoryListingParser parser;
EXPECT_FALSE(parser.ConsumeLine(UTF8ToUTF16(bad_cases[i]))) << bad_cases[i];
}
}
} // namespace
...@@ -289,6 +289,10 @@ ...@@ -289,6 +289,10 @@
'ftp/ftp_auth_cache.h', 'ftp/ftp_auth_cache.h',
'ftp/ftp_ctrl_response_buffer.cc', 'ftp/ftp_ctrl_response_buffer.cc',
'ftp/ftp_ctrl_response_buffer.h', 'ftp/ftp_ctrl_response_buffer.h',
'ftp/ftp_directory_listing_buffer.cc',
'ftp/ftp_directory_listing_buffer.h',
'ftp/ftp_directory_listing_parsers.cc',
'ftp/ftp_directory_listing_parsers.h',
'ftp/ftp_network_layer.cc', 'ftp/ftp_network_layer.cc',
'ftp/ftp_network_layer.h', 'ftp/ftp_network_layer.h',
'ftp/ftp_network_session.h', 'ftp/ftp_network_session.h',
...@@ -592,6 +596,8 @@ ...@@ -592,6 +596,8 @@
'disk_cache/storage_block_unittest.cc', 'disk_cache/storage_block_unittest.cc',
'ftp/ftp_auth_cache_unittest.cc', 'ftp/ftp_auth_cache_unittest.cc',
'ftp/ftp_ctrl_response_buffer_unittest.cc', 'ftp/ftp_ctrl_response_buffer_unittest.cc',
'ftp/ftp_directory_listing_buffer_unittest.cc',
'ftp/ftp_directory_listing_parsers_unittest.cc',
'ftp/ftp_network_transaction_unittest.cc', 'ftp/ftp_network_transaction_unittest.cc',
'ftp/ftp_util_unittest.cc', 'ftp/ftp_util_unittest.cc',
'http/des_unittest.cc', 'http/des_unittest.cc',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment