1eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// Copyright 2013 The Chromium Authors. All rights reserved.
290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)// found in the LICENSE file.
490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/base64.h"
690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/base_paths_win.h"
790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/bind.h"
81320f92c476a1ad9d19dba2a48c72b75566198e9Primiano Tucci#include "base/files/file_util.h"
990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/files/scoped_temp_dir.h"
1090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/logging.h"
1190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/memory/scoped_ptr.h"
12ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch#include "base/message_loop/message_loop.h"
1390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/path_service.h"
1490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/run_loop.h"
15868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)#include "base/strings/stringprintf.h"
1690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "base/test/scoped_path_override.h"
174e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)#include "chrome/browser/media_galleries/fileapi/iapps_finder_impl.h"
1890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)#include "chrome/common/chrome_paths.h"
19eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch#include "chrome/test/base/in_process_browser_test.h"
2090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
214e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)namespace iapps {
2290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
2390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)namespace {
2490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
2590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)std::string EncodePath(const base::FilePath& path) {
2690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  std::string input(reinterpret_cast<const char*>(path.value().data()),
2790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)                                                  path.value().size()*2);
2890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  std::string result;
2990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  base::Base64Encode(input, &result);
3090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  return result;
3190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}
3290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
3390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)void TouchFile(const base::FilePath& file) {
34a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)  ASSERT_EQ(1, base::WriteFile(file, " ", 1));
3590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}
3690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
37eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochclass ITunesFinderWinTest : public InProcessBrowserTest {
3890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) public:
39eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  ITunesFinderWinTest() : test_finder_callback_called_(false) {}
4090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
41ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch  virtual ~ITunesFinderWinTest() {}
42ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch
4390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  virtual void SetUp() OVERRIDE {
4490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    ASSERT_TRUE(app_data_dir_.CreateUniqueTempDir());
4590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    ASSERT_TRUE(music_dir_.CreateUniqueTempDir());
4690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    app_data_dir_override_.reset(
4790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)        new base::ScopedPathOverride(base::DIR_APP_DATA, app_data_dir()));
4890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    music_dir_override_.reset(
4990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)        new base::ScopedPathOverride(chrome::DIR_USER_MUSIC, music_dir()));
50eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    InProcessBrowserTest::SetUp();
5190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
5290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
5390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  const base::FilePath& app_data_dir() {
5490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    return app_data_dir_.path();
5590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
5690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
5790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  const base::FilePath& music_dir() {
5890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    return music_dir_.path();
5990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
6090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
6190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  void WritePrefFile(const std::string& data) {
6290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    base::FilePath pref_dir =
6390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)        app_data_dir().AppendASCII("Apple Computer").AppendASCII("iTunes");
64a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    ASSERT_TRUE(base::CreateDirectory(pref_dir));
6590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    ASSERT_EQ(data.size(),
66a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)              base::WriteFile(pref_dir.AppendASCII("iTunesPrefs.xml"),
67a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)                              data.data(), data.size()));
6890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
6990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
7090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  void TouchDefault() {
7190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    base::FilePath default_dir = music_dir().AppendASCII("iTunes");
72a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    ASSERT_TRUE(base::CreateDirectory(default_dir));
7390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)    TouchFile(default_dir.AppendASCII("iTunes Music Library.xml"));
7490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
7590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
76eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  void TestFindITunesLibrary() {
77eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    test_finder_callback_called_ = false;
78eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    result_.clear();
79eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    base::RunLoop loop;
804e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)    FindITunesLibrary(base::Bind(&ITunesFinderWinTest::TestFinderCallback,
814e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)                                 base::Unretained(this), loop.QuitClosure()));
82eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    loop.Run();
83eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  }
84eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
85eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  bool EmptyResult() const {
86eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    return result_.empty();
87eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  }
88eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
89eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  bool test_finder_callback_called() const {
90eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    return test_finder_callback_called_;
9190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
9290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
9390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles) private:
94eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  void TestFinderCallback(const base::Closure& quit_closure,
95eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                          const std::string& result) {
96eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    test_finder_callback_called_ = true;
97eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    result_ = result;
98eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    quit_closure.Run();
9990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  }
10090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
10190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  scoped_ptr<base::ScopedPathOverride> app_data_dir_override_;
10290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  scoped_ptr<base::ScopedPathOverride> music_dir_override_;
10390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  base::ScopedTempDir app_data_dir_;
10490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  base::ScopedTempDir music_dir_;
10590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
106eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  bool test_finder_callback_called_;
107eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  std::string result_;
10890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
10990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  DISALLOW_COPY_AND_ASSIGN(ITunesFinderWinTest);
11090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)};
11190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
112eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen MurdochIN_PROC_BROWSER_TEST_F(ITunesFinderWinTest, NotFound) {
113eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  TestFindITunesLibrary();
114eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(test_finder_callback_called());
115eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(EmptyResult());
11690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}
11790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
118eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen MurdochIN_PROC_BROWSER_TEST_F(ITunesFinderWinTest, DefaultLocation) {
11990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  TouchDefault();
120eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  TestFindITunesLibrary();
121eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(test_finder_callback_called());
122eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_FALSE(EmptyResult());
12390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}
12490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
125eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen MurdochIN_PROC_BROWSER_TEST_F(ITunesFinderWinTest, CustomLocation) {
12690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  base::FilePath library_xml = music_dir().AppendASCII("library.xml");
12790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  TouchFile(library_xml);
12890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  std::string xml = base::StringPrintf(
12990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "<plist>"
13090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "  <dict>"
13190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <key>User Preferences</key>"
13290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <dict>"
13390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "      <key>iTunes Library XML Location:1</key>"
13490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "      <data>%s</data>"
13590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    </dict>"
13690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "  </dict>"
13790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "</plist>", EncodePath(library_xml).c_str());
13890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  WritePrefFile(xml);
139eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  TestFindITunesLibrary();
140eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(test_finder_callback_called());
141eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_FALSE(EmptyResult());
14290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}
14390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
144eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen MurdochIN_PROC_BROWSER_TEST_F(ITunesFinderWinTest, BadCustomLocation) {
14590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  // Missing file.
14690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  base::FilePath library_xml = music_dir().AppendASCII("library.xml");
14790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  std::string xml = base::StringPrintf(
14890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "<plist>"
14990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "  <dict>"
15090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <key>User Preferences</key>"
15190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <dict>"
15290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "      <key>iTunes Library XML Location:1</key>"
15390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "      <data>%s</data>"
15490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    </dict>"
15590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "  </dict>"
15690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "</plist>", EncodePath(library_xml).c_str());
15790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  WritePrefFile(xml);
158eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  TestFindITunesLibrary();
159eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(test_finder_callback_called());
160eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(EmptyResult());
16190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  TouchFile(library_xml);
16290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
16390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  // User Preferences dictionary at the wrong level.
16490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  xml = base::StringPrintf(
16590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "<plist>"
16690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <key>User Preferences</key>"
16790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <dict>"
16890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "      <key>iTunes Library XML Location:1</key>"
16990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "      <data>%s</data>"
17090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    </dict>"
17190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "</plist>", EncodePath(library_xml).c_str());
17290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  WritePrefFile(xml);
173eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  TestFindITunesLibrary();
174eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(test_finder_callback_called());
175eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(EmptyResult());
17690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
17790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  // Library location at the wrong scope.
17890dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  xml = base::StringPrintf(
17990dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "<plist>"
18090dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "  <dict>"
18190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <key>User Preferences</key>"
18290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <dict/>"
18390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <key>iTunes Library XML Location:1</key>"
18490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "    <data>%s</data>"
18590dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "  </dict>"
18690dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)      "</plist>", EncodePath(library_xml).c_str());
18790dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)  WritePrefFile(xml);
188eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  TestFindITunesLibrary();
189eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(test_finder_callback_called());
190eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  EXPECT_TRUE(EmptyResult());
19190dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}
19290dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
19390dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)}  // namespace
19490dce4d38c5ff5333bea97d859d4e484e27edf0cTorne (Richard Coles)
1954e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)}  // namespace iapps
196