1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "net/ftp/ftp_util.h"
6
7#include <vector>
8
9#include "base/i18n/char_iterator.h"
10#include "base/logging.h"
11#include "base/string_number_conversions.h"
12#include "base/string_tokenizer.h"
13#include "base/string_util.h"
14#include "base/time.h"
15#include "base/utf_string_conversions.h"
16#include "unicode/datefmt.h"
17#include "unicode/dtfmtsym.h"
18#include "unicode/uchar.h"
19
20// For examples of Unix<->VMS path conversions, see the unit test file. On VMS
21// a path looks differently depending on whether it's a file or directory.
22
23namespace net {
24
25// static
26std::string FtpUtil::UnixFilePathToVMS(const std::string& unix_path) {
27  if (unix_path.empty())
28    return std::string();
29
30  StringTokenizer tokenizer(unix_path, "/");
31  std::vector<std::string> tokens;
32  while (tokenizer.GetNext())
33    tokens.push_back(tokenizer.token());
34
35  if (unix_path[0] == '/') {
36    // It's an absolute path.
37
38    if (tokens.empty()) {
39      DCHECK_EQ(1U, unix_path.length());
40      return "[]";
41    }
42
43    if (tokens.size() == 1)
44      return unix_path.substr(1);  // Drop the leading slash.
45
46    std::string result(tokens[0] + ":[");
47    if (tokens.size() == 2) {
48      // Don't ask why, it just works that way on VMS.
49      result.append("000000");
50    } else {
51      result.append(tokens[1]);
52      for (size_t i = 2; i < tokens.size() - 1; i++)
53        result.append("." + tokens[i]);
54    }
55    result.append("]" + tokens[tokens.size() - 1]);
56    return result;
57  }
58
59  if (tokens.size() == 1)
60    return unix_path;
61
62  std::string result("[");
63  for (size_t i = 0; i < tokens.size() - 1; i++)
64    result.append("." + tokens[i]);
65  result.append("]" + tokens[tokens.size() - 1]);
66  return result;
67}
68
69// static
70std::string FtpUtil::UnixDirectoryPathToVMS(const std::string& unix_path) {
71  if (unix_path.empty())
72    return std::string();
73
74  std::string path(unix_path);
75
76  if (path[path.length() - 1] != '/')
77    path.append("/");
78
79  // Reuse logic from UnixFilePathToVMS by appending a fake file name to the
80  // real path and removing it after conversion.
81  path.append("x");
82  path = UnixFilePathToVMS(path);
83  return path.substr(0, path.length() - 1);
84}
85
86// static
87std::string FtpUtil::VMSPathToUnix(const std::string& vms_path) {
88  if (vms_path.empty())
89    return ".";
90
91  if (vms_path == "[]")
92    return "/";
93
94  std::string result(vms_path);
95  if (vms_path[0] == '[') {
96    // It's a relative path.
97    ReplaceFirstSubstringAfterOffset(&result, 0, "[.", "");
98  } else {
99    // It's an absolute path.
100    result.insert(0, "/");
101    ReplaceSubstringsAfterOffset(&result, 0, ":[000000]", "/");
102    ReplaceSubstringsAfterOffset(&result, 0, ":[", "/");
103  }
104  std::replace(result.begin(), result.end(), '.', '/');
105  std::replace(result.begin(), result.end(), ']', '/');
106
107  // Make sure the result doesn't end with a slash.
108  if (result.length() && result[result.length() - 1] == '/')
109    result = result.substr(0, result.length() - 1);
110
111  return result;
112}
113
114// static
115bool FtpUtil::AbbreviatedMonthToNumber(const string16& text, int* number) {
116  icu::UnicodeString unicode_text(text.data(), text.size());
117
118  int32_t locales_count;
119  const icu::Locale* locales =
120      icu::DateFormat::getAvailableLocales(locales_count);
121
122  // Some FTP servers localize the date listings. To guess the locale,
123  // we loop over all available ones.
124  for (int32_t locale = 0; locale < locales_count; locale++) {
125    UErrorCode status(U_ZERO_ERROR);
126
127    icu::DateFormatSymbols format_symbols(locales[locale], status);
128
129    // If we cannot get format symbols for some locale, it's not a fatal error.
130    // Just try another one.
131    if (U_FAILURE(status))
132      continue;
133
134    int32_t months_count;
135    const icu::UnicodeString* months =
136        format_symbols.getShortMonths(months_count);
137
138    // Loop over all abbreviated month names in given locale.
139    // An alternative solution (to parse |text| in given locale) is more
140    // lenient, and may accept more than we want even with setLenient(false).
141    for (int32_t month = 0; month < months_count; month++) {
142      // Compare (case-insensitive), but just first three characters. Sometimes
143      // ICU returns longer strings (for example for Russian locale), and in FTP
144      // listings they are abbreviated to just three characters.
145      // Note: ICU may also return strings shorter than three characters,
146      // and those also should be accepted.
147      if (months[month].caseCompare(0, 3, unicode_text, 0) == 0) {
148        *number = month + 1;
149        return true;
150      }
151    }
152  }
153
154  return false;
155}
156
157// static
158bool FtpUtil::LsDateListingToTime(const string16& month, const string16& day,
159                                  const string16& rest,
160                                  const base::Time& current_time,
161                                  base::Time* result) {
162  base::Time::Exploded time_exploded = { 0 };
163
164  if (!AbbreviatedMonthToNumber(month, &time_exploded.month))
165    return false;
166
167  if (!base::StringToInt(day, &time_exploded.day_of_month))
168    return false;
169  if (time_exploded.day_of_month > 31)
170    return false;
171
172  if (!base::StringToInt(rest, &time_exploded.year)) {
173    // Maybe it's time. Does it look like time (HH:MM)?
174    if (rest.length() == 5 && rest[2] == ':') {
175      if (!base::StringToInt(rest.begin(),
176                             rest.begin() + 2,
177                             &time_exploded.hour))
178        return false;
179
180      if (!base::StringToInt(rest.begin() + 3,
181                             rest.begin() + 5,
182                             &time_exploded.minute))
183        return false;
184    } else if (rest.length() == 4 && rest[1] == ':') {
185      // Sometimes it's just H:MM.
186      if (!base::StringToInt(rest.begin(),
187                             rest.begin() + 1,
188                             &time_exploded.hour))
189        return false;
190
191      if (!base::StringToInt(rest.begin() + 2,
192                             rest.begin() + 4,
193                             &time_exploded.minute))
194        return false;
195    } else {
196      return false;
197    }
198
199    // Guess the year.
200    base::Time::Exploded current_exploded;
201    current_time.LocalExplode(&current_exploded);
202
203    // If it's not possible for the parsed date to be in the current year,
204    // use the previous year.
205    if (time_exploded.month > current_exploded.month ||
206        (time_exploded.month == current_exploded.month &&
207         time_exploded.day_of_month > current_exploded.day_of_month)) {
208      time_exploded.year = current_exploded.year - 1;
209    } else {
210      time_exploded.year = current_exploded.year;
211    }
212  }
213
214  // We don't know the time zone of the listing, so just use local time.
215  *result = base::Time::FromLocalExploded(time_exploded);
216  return true;
217}
218
219// static
220string16 FtpUtil::GetStringPartAfterColumns(const string16& text, int columns) {
221  base::i18n::UTF16CharIterator iter(&text);
222
223  // TODO(jshin): Is u_isspace the right function to use here?
224  for (int i = 0; i < columns; i++) {
225    // Skip the leading whitespace.
226    while (!iter.end() && u_isspace(iter.get()))
227      iter.Advance();
228
229    // Skip the actual text of i-th column.
230    while (!iter.end() && !u_isspace(iter.get()))
231      iter.Advance();
232  }
233
234  string16 result(text.substr(iter.array_pos()));
235  TrimWhitespace(result, TRIM_ALL, &result);
236  return result;
237}
238
239}  // namespace
240