blob: f271d09f692e4712cf147e79a57d88026f009f61 [file] [log] [blame]
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in
Anatol Pomazau5ae3f932012-02-28 07:21:08 -080012 * the documentation and/or other materials provided with the
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080013 * distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
18 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
19 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
20 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
Anatol Pomazau5ae3f932012-02-28 07:21:08 -080022 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080023 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
25 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26 * SUCH DAMAGE.
27 */
28
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080029#include "fastboot.h"
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080030
Colin Cross81c632e2013-01-23 19:13:43 -080031#include <errno.h>
Elliott Hughes3ab8b852015-08-25 19:34:13 -070032#include <stdarg.h>
Mark Salyzyn5957c1f2014-04-30 14:05:28 -070033#include <stdio.h>
34#include <stdlib.h>
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080035#include <string.h>
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080036#include <sys/stat.h>
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080037#include <sys/types.h>
38#include <unistd.h>
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080039
Elliott Hughes5620d222018-03-28 08:20:00 -070040#include <memory>
41#include <vector>
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080042
Elliott Hughes5620d222018-03-28 08:20:00 -070043#include <android-base/stringprintf.h>
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080044
Elliott Hughes11ff3452018-04-09 13:58:41 -070045#include "transport.h"
46
Elliott Hughes5620d222018-03-28 08:20:00 -070047enum Op {
48 OP_DOWNLOAD,
49 OP_COMMAND,
50 OP_QUERY,
51 OP_NOTICE,
52 OP_DOWNLOAD_SPARSE,
53 OP_WAIT_FOR_DISCONNECT,
54 OP_DOWNLOAD_FD,
55 OP_UPLOAD,
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080056};
57
Elliott Hughes5620d222018-03-28 08:20:00 -070058struct Action {
59 Action(Op op, const std::string& cmd) : op(op), cmd(cmd) {}
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080060
Elliott Hughes5620d222018-03-28 08:20:00 -070061 Op op;
62 std::string cmd;
63 std::string msg;
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080064
Elliott Hughes5620d222018-03-28 08:20:00 -070065 std::string product;
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080066
Elliott Hughes5620d222018-03-28 08:20:00 -070067 void* data = nullptr;
68 // The protocol only supports 32-bit sizes, so you'll have to break
69 // anything larger into multiple chunks.
70 uint32_t size = 0;
71
72 int fd = -1;
73
74 int (*func)(Action& a, int status, const char* resp) = nullptr;
75
76 double start = -1;
77};
78
79static std::vector<std::unique_ptr<Action>> action_list;
Anatol Pomazauc8ba5362011-12-15 17:50:18 -080080
David Pursell0b156632015-10-30 11:22:01 -070081bool fb_getvar(Transport* transport, const std::string& key, std::string* value) {
Elliott Hughes5620d222018-03-28 08:20:00 -070082 std::string cmd = "getvar:" + key;
Colin Cross80f2d032012-05-24 18:24:53 -070083
Elliott Hughes2fd45a92015-10-30 11:49:47 -070084 char buf[FB_RESPONSE_SZ + 1];
85 memset(buf, 0, sizeof(buf));
Elliott Hughes5620d222018-03-28 08:20:00 -070086 if (fb_command_response(transport, cmd, buf)) {
87 return false;
Elliott Hughes2fd45a92015-10-30 11:49:47 -070088 }
89 *value = buf;
90 return true;
Colin Cross80f2d032012-05-24 18:24:53 -070091}
92
Elliott Hughes5620d222018-03-28 08:20:00 -070093static int cb_default(Action& a, int status, const char* resp) {
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -080094 if (status) {
95 fprintf(stderr,"FAILED (%s)\n", resp);
96 } else {
Daniel Sandlercb6e22b2010-02-25 14:05:33 -050097 double split = now();
Elliott Hughes5620d222018-03-28 08:20:00 -070098 fprintf(stderr, "OKAY [%7.3fs]\n", (split - a.start));
99 a.start = split;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800100 }
101 return status;
102}
103
Elliott Hughes5620d222018-03-28 08:20:00 -0700104static Action& queue_action(Op op, const std::string& cmd) {
105 std::unique_ptr<Action> a{new Action(op, cmd)};
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800106 a->func = cb_default;
Daniel Sandlercb6e22b2010-02-25 14:05:33 -0500107
Elliott Hughes5620d222018-03-28 08:20:00 -0700108 action_list.push_back(std::move(a));
109 return *action_list.back();
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800110}
111
Elliott Hughes5620d222018-03-28 08:20:00 -0700112void fb_set_active(const std::string& slot) {
113 Action& a = queue_action(OP_COMMAND, "set_active:" + slot);
Elliott Hughesf238d872018-03-29 14:46:29 -0700114 a.msg = "Setting current slot to '" + slot + "'";
Daniel Rosenbergb7bd4ae2015-09-14 21:05:41 -0700115}
116
Elliott Hughes5620d222018-03-28 08:20:00 -0700117void fb_queue_erase(const std::string& partition) {
118 Action& a = queue_action(OP_COMMAND, "erase:" + partition);
Elliott Hughesf238d872018-03-29 14:46:29 -0700119 a.msg = "Erasing '" + partition + "'";
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800120}
121
Elliott Hughes5620d222018-03-28 08:20:00 -0700122void fb_queue_flash_fd(const std::string& partition, int fd, uint32_t sz) {
123 Action& a = queue_action(OP_DOWNLOAD_FD, "");
124 a.fd = fd;
125 a.size = sz;
Elliott Hughesf238d872018-03-29 14:46:29 -0700126 a.msg = android::base::StringPrintf("Sending '%s' (%u KB)", partition.c_str(), sz / 1024);
Chris Fries0ea946c2017-04-12 10:25:57 -0500127
Elliott Hughes5620d222018-03-28 08:20:00 -0700128 Action& b = queue_action(OP_COMMAND, "flash:" + partition);
Elliott Hughesf238d872018-03-29 14:46:29 -0700129 b.msg = "Writing '" + partition + "'";
Chris Fries0ea946c2017-04-12 10:25:57 -0500130}
131
Elliott Hughes5620d222018-03-28 08:20:00 -0700132void fb_queue_flash(const std::string& partition, void* data, uint32_t sz) {
133 Action& a = queue_action(OP_DOWNLOAD, "");
134 a.data = data;
135 a.size = sz;
Elliott Hughesf238d872018-03-29 14:46:29 -0700136 a.msg = android::base::StringPrintf("Sending '%s' (%u KB)", partition.c_str(), sz / 1024);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800137
Elliott Hughes5620d222018-03-28 08:20:00 -0700138 Action& b = queue_action(OP_COMMAND, "flash:" + partition);
Elliott Hughesf238d872018-03-29 14:46:29 -0700139 b.msg = "Writing '" + partition + "'";
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800140}
141
Elliott Hughes5620d222018-03-28 08:20:00 -0700142void fb_queue_flash_sparse(const std::string& partition, struct sparse_file* s, uint32_t sz,
143 size_t current, size_t total) {
144 Action& a = queue_action(OP_DOWNLOAD_SPARSE, "");
145 a.data = s;
146 a.size = 0;
Elliott Hughesf238d872018-03-29 14:46:29 -0700147 a.msg = android::base::StringPrintf("Sending sparse '%s' %zu/%zu (%u KB)", partition.c_str(),
Elliott Hughes5620d222018-03-28 08:20:00 -0700148 current, total, sz / 1024);
Colin Crossf8387882012-05-24 17:18:41 -0700149
Elliott Hughes5620d222018-03-28 08:20:00 -0700150 Action& b = queue_action(OP_COMMAND, "flash:" + partition);
Elliott Hughesf238d872018-03-29 14:46:29 -0700151 b.msg = android::base::StringPrintf("Writing sparse '%s' %zu/%zu", partition.c_str(), current,
152 total);
Colin Crossf8387882012-05-24 17:18:41 -0700153}
154
Elliott Hughesb3748de2015-06-23 20:27:58 -0700155static int match(const char* str, const char** value, unsigned count) {
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800156 unsigned n;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800157
158 for (n = 0; n < count; n++) {
159 const char *val = value[n];
160 int len = strlen(val);
161 int match;
162
163 if ((len > 1) && (val[len-1] == '*')) {
164 len--;
165 match = !strncmp(val, str, len);
166 } else {
167 match = !strcmp(val, str);
168 }
169
170 if (match) return 1;
171 }
172
173 return 0;
174}
175
Elliott Hughes5620d222018-03-28 08:20:00 -0700176static int cb_check(Action& a, int status, const char* resp, int invert) {
177 const char** value = reinterpret_cast<const char**>(a.data);
178 unsigned count = a.size;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800179 unsigned n;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800180
181 if (status) {
182 fprintf(stderr,"FAILED (%s)\n", resp);
183 return status;
184 }
185
Elliott Hughes5620d222018-03-28 08:20:00 -0700186 if (!a.product.empty()) {
187 if (a.product != cur_product) {
Wink Savilleb98762f2011-04-04 17:54:59 -0700188 double split = now();
Elliott Hughes5620d222018-03-28 08:20:00 -0700189 fprintf(stderr, "IGNORE, product is %s required only for %s [%7.3fs]\n", cur_product,
190 a.product.c_str(), (split - a.start));
191 a.start = split;
Wink Savilleb98762f2011-04-04 17:54:59 -0700192 return 0;
193 }
194 }
195
Elliott Hughes5620d222018-03-28 08:20:00 -0700196 int yes = match(resp, value, count);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800197 if (invert) yes = !yes;
198
199 if (yes) {
Daniel Sandlercb6e22b2010-02-25 14:05:33 -0500200 double split = now();
Elliott Hughes5620d222018-03-28 08:20:00 -0700201 fprintf(stderr, "OKAY [%7.3fs]\n", (split - a.start));
202 a.start = split;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800203 return 0;
204 }
205
Elliott Hughes5620d222018-03-28 08:20:00 -0700206 fprintf(stderr, "FAILED\n\n");
207 fprintf(stderr, "Device %s is '%s'.\n", a.cmd.c_str() + 7, resp);
208 fprintf(stderr, "Update %s '%s'", invert ? "rejects" : "requires", value[0]);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800209 for (n = 1; n < count; n++) {
Elliott Hughes5620d222018-03-28 08:20:00 -0700210 fprintf(stderr, " or '%s'", value[n]);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800211 }
Elliott Hughes5620d222018-03-28 08:20:00 -0700212 fprintf(stderr, ".\n\n");
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800213 return -1;
214}
215
Elliott Hughes5620d222018-03-28 08:20:00 -0700216static int cb_require(Action& a, int status, const char* resp) {
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800217 return cb_check(a, status, resp, 0);
218}
219
Elliott Hughes5620d222018-03-28 08:20:00 -0700220static int cb_reject(Action& a, int status, const char* resp) {
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800221 return cb_check(a, status, resp, 1);
222}
223
Elliott Hughes5620d222018-03-28 08:20:00 -0700224void fb_queue_require(const std::string& product, const std::string& var, bool invert,
225 size_t nvalues, const char** values) {
226 Action& a = queue_action(OP_QUERY, "getvar:" + var);
227 a.product = product;
228 a.data = values;
229 a.size = nvalues;
230 a.msg = "Checking " + var;
231 a.func = invert ? cb_reject : cb_require;
232 if (a.data == nullptr) die("out of memory");
Elliott Hughesd6365a72017-05-08 18:04:49 -0700233}
234
Elliott Hughes5620d222018-03-28 08:20:00 -0700235static int cb_display(Action& a, int status, const char* resp) {
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800236 if (status) {
Elliott Hughes5620d222018-03-28 08:20:00 -0700237 fprintf(stderr, "%s FAILED (%s)\n", a.cmd.c_str(), resp);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800238 return status;
239 }
Elliott Hughes5620d222018-03-28 08:20:00 -0700240 fprintf(stderr, "%s: %s\n", static_cast<const char*>(a.data), resp);
241 free(static_cast<char*>(a.data));
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800242 return 0;
243}
244
Elliott Hughes5620d222018-03-28 08:20:00 -0700245void fb_queue_display(const std::string& label, const std::string& var) {
246 Action& a = queue_action(OP_QUERY, "getvar:" + var);
247 a.data = xstrdup(label.c_str());
248 a.func = cb_display;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800249}
250
Elliott Hughes5620d222018-03-28 08:20:00 -0700251static int cb_save(Action& a, int status, const char* resp) {
Wink Savilleb98762f2011-04-04 17:54:59 -0700252 if (status) {
Elliott Hughes5620d222018-03-28 08:20:00 -0700253 fprintf(stderr, "%s FAILED (%s)\n", a.cmd.c_str(), resp);
Wink Savilleb98762f2011-04-04 17:54:59 -0700254 return status;
255 }
Elliott Hughes5620d222018-03-28 08:20:00 -0700256 strncpy(reinterpret_cast<char*>(a.data), resp, a.size);
Wink Savilleb98762f2011-04-04 17:54:59 -0700257 return 0;
258}
259
Elliott Hughes5620d222018-03-28 08:20:00 -0700260void fb_queue_query_save(const std::string& var, char* dest, uint32_t dest_size) {
261 Action& a = queue_action(OP_QUERY, "getvar:" + var);
262 a.data = dest;
263 a.size = dest_size;
264 a.func = cb_save;
Wink Savilleb98762f2011-04-04 17:54:59 -0700265}
266
Elliott Hughes5620d222018-03-28 08:20:00 -0700267static int cb_do_nothing(Action&, int, const char*) {
268 fprintf(stderr, "\n");
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800269 return 0;
270}
271
Elliott Hughes5620d222018-03-28 08:20:00 -0700272void fb_queue_reboot() {
273 Action& a = queue_action(OP_COMMAND, "reboot");
274 a.func = cb_do_nothing;
Elliott Hughesf238d872018-03-29 14:46:29 -0700275 a.msg = "Rebooting";
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800276}
277
Elliott Hughes5620d222018-03-28 08:20:00 -0700278void fb_queue_command(const std::string& cmd, const std::string& msg) {
279 Action& a = queue_action(OP_COMMAND, cmd);
280 a.msg = msg;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800281}
282
Elliott Hughes5620d222018-03-28 08:20:00 -0700283void fb_queue_download(const std::string& name, void* data, uint32_t size) {
284 Action& a = queue_action(OP_DOWNLOAD, "");
285 a.data = data;
286 a.size = size;
287 a.msg = "Downloading '" + name + "'";
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800288}
289
Elliott Hughes5620d222018-03-28 08:20:00 -0700290void fb_queue_download_fd(const std::string& name, int fd, uint32_t sz) {
291 Action& a = queue_action(OP_DOWNLOAD_FD, "");
292 a.fd = fd;
293 a.size = sz;
Elliott Hughesf238d872018-03-29 14:46:29 -0700294 a.msg = android::base::StringPrintf("Sending '%s' (%u KB)", name.c_str(), sz / 1024);
Jocelyn Bohr98cc2832017-01-26 19:20:53 -0800295}
296
Elliott Hughes5620d222018-03-28 08:20:00 -0700297void fb_queue_upload(const std::string& outfile) {
298 Action& a = queue_action(OP_UPLOAD, "");
299 a.data = xstrdup(outfile.c_str());
300 a.msg = "Uploading '" + outfile + "'";
Jocelyn Bohr91fefad2017-01-27 14:17:56 -0800301}
302
Elliott Hughes5620d222018-03-28 08:20:00 -0700303void fb_queue_notice(const std::string& notice) {
304 Action& a = queue_action(OP_NOTICE, "");
305 a.msg = notice;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800306}
307
Elliott Hughes5620d222018-03-28 08:20:00 -0700308void fb_queue_wait_for_disconnect() {
Mark Wachsler157b0012013-10-02 09:35:38 -0400309 queue_action(OP_WAIT_FOR_DISCONNECT, "");
310}
311
Elliott Hughes5620d222018-03-28 08:20:00 -0700312int64_t fb_execute_queue(Transport* transport) {
Chris Fries6a999712017-04-04 09:52:47 -0500313 int64_t status = 0;
Elliott Hughes5620d222018-03-28 08:20:00 -0700314 for (auto& a : action_list) {
Daniel Sandlercb6e22b2010-02-25 14:05:33 -0500315 a->start = now();
Elliott Hughes5620d222018-03-28 08:20:00 -0700316 if (!a->msg.empty()) {
Elliott Hughesf238d872018-03-29 14:46:29 -0700317 fprintf(stderr, "%-50s ", a->msg.c_str());
Elliott Hughes855cdf82018-04-02 14:24:03 -0700318 verbose("\n");
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800319 }
320 if (a->op == OP_DOWNLOAD) {
David Pursell0b156632015-10-30 11:22:01 -0700321 status = fb_download_data(transport, a->data, a->size);
Elliott Hughes5620d222018-03-28 08:20:00 -0700322 status = a->func(*a, status, status ? fb_get_error().c_str() : "");
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800323 if (status) break;
Chris Fries0ea946c2017-04-12 10:25:57 -0500324 } else if (a->op == OP_DOWNLOAD_FD) {
325 status = fb_download_data_fd(transport, a->fd, a->size);
Elliott Hughes5620d222018-03-28 08:20:00 -0700326 status = a->func(*a, status, status ? fb_get_error().c_str() : "");
Chris Fries0ea946c2017-04-12 10:25:57 -0500327 if (status) break;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800328 } else if (a->op == OP_COMMAND) {
David Pursell0b156632015-10-30 11:22:01 -0700329 status = fb_command(transport, a->cmd);
Elliott Hughes5620d222018-03-28 08:20:00 -0700330 status = a->func(*a, status, status ? fb_get_error().c_str() : "");
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800331 if (status) break;
332 } else if (a->op == OP_QUERY) {
Elliott Hughes5620d222018-03-28 08:20:00 -0700333 char resp[FB_RESPONSE_SZ + 1] = {};
David Pursell0b156632015-10-30 11:22:01 -0700334 status = fb_command_response(transport, a->cmd, resp);
Elliott Hughes5620d222018-03-28 08:20:00 -0700335 status = a->func(*a, status, status ? fb_get_error().c_str() : resp);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800336 if (status) break;
337 } else if (a->op == OP_NOTICE) {
Elliott Hughes5620d222018-03-28 08:20:00 -0700338 // We already showed the notice because it's in `Action::msg`.
Elliott Hughesf238d872018-03-29 14:46:29 -0700339 fprintf(stderr, "\n");
Colin Crossf8387882012-05-24 17:18:41 -0700340 } else if (a->op == OP_DOWNLOAD_SPARSE) {
David Pursell0b156632015-10-30 11:22:01 -0700341 status = fb_download_data_sparse(transport, reinterpret_cast<sparse_file*>(a->data));
Elliott Hughes5620d222018-03-28 08:20:00 -0700342 status = a->func(*a, status, status ? fb_get_error().c_str() : "");
Colin Crossf8387882012-05-24 17:18:41 -0700343 if (status) break;
Mark Wachsler157b0012013-10-02 09:35:38 -0400344 } else if (a->op == OP_WAIT_FOR_DISCONNECT) {
David Pursell0b156632015-10-30 11:22:01 -0700345 transport->WaitForDisconnect();
Jocelyn Bohr91fefad2017-01-27 14:17:56 -0800346 } else if (a->op == OP_UPLOAD) {
347 status = fb_upload_data(transport, reinterpret_cast<char*>(a->data));
Elliott Hughes5620d222018-03-28 08:20:00 -0700348 status = a->func(*a, status, status ? fb_get_error().c_str() : "");
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800349 } else {
Elliott Hughes5620d222018-03-28 08:20:00 -0700350 die("unknown action: %d", a->op);
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800351 }
352 }
Elliott Hughes5620d222018-03-28 08:20:00 -0700353 action_list.clear();
Brian Carlstromeb31c0b2010-04-23 12:38:51 -0700354 return status;
The Android Open Source Projectdd7bc332009-03-03 19:32:55 -0800355}