1 /*
2  * Unit tests for playlist_check_translate_song().
3  */
4 
5 #include "MakeTag.hxx"
6 #include "playlist/PlaylistSong.hxx"
7 #include "song/DetachedSong.hxx"
8 #include "SongLoader.hxx"
9 #include "client/Client.hxx"
10 #include "tag/Builder.hxx"
11 #include "tag/Tag.hxx"
12 #include "util/Domain.hxx"
13 #include "fs/AllocatedPath.hxx"
14 #include "ls.hxx"
15 #include "Log.hxx"
16 #include "db/DatabaseSong.hxx"
17 #include "storage/StorageInterface.hxx"
18 #include "storage/plugins/LocalStorage.hxx"
19 #include "Mapper.hxx"
20 #include "time/ChronoUtil.hxx"
21 
22 #include <gtest/gtest.h>
23 
24 #include <string.h>
25 #include <stdio.h>
26 
27 void
Log(LogLevel,const Domain & domain,const char * msg)28 Log(LogLevel, const Domain &domain, const char *msg) noexcept
29 {
30 	fprintf(stderr, "[%s] %s\n", domain.GetName(), msg);
31 }
32 
33 bool
uri_supported_scheme(const char * uri)34 uri_supported_scheme(const char *uri) noexcept
35 {
36 	return strncmp(uri, "http://", 7) == 0;
37 }
38 
39 static constexpr auto music_directory = PATH_LITERAL("/music");
40 static Storage *storage;
41 
42 static Tag
MakeTag1a()43 MakeTag1a()
44 {
45 	return MakeTag(TAG_ARTIST, "artist_a1", TAG_TITLE, "title_a1",
46 		       TAG_ALBUM, "album_a1");
47 }
48 
49 static Tag
MakeTag1b()50 MakeTag1b()
51 {
52 	return MakeTag(TAG_ARTIST, "artist_b1", TAG_TITLE, "title_b1",
53 		       TAG_COMMENT, "comment_b1");
54 }
55 
56 static Tag
MakeTag1c()57 MakeTag1c()
58 {
59 	return MakeTag(TAG_ARTIST, "artist_b1", TAG_TITLE, "title_b1",
60 		       TAG_COMMENT, "comment_b1", TAG_ALBUM, "album_a1");
61 }
62 
63 static Tag
MakeTag2a()64 MakeTag2a()
65 {
66 	return MakeTag(TAG_ARTIST, "artist_a2", TAG_TITLE, "title_a2",
67 		       TAG_ALBUM, "album_a2");
68 }
69 
70 static Tag
MakeTag2b()71 MakeTag2b()
72 {
73 	return MakeTag(TAG_ARTIST, "artist_b2", TAG_TITLE, "title_b2",
74 		       TAG_COMMENT, "comment_b2");
75 }
76 
77 static Tag
MakeTag2c()78 MakeTag2c()
79 {
80 	return MakeTag(TAG_ARTIST, "artist_b2", TAG_TITLE, "title_b2",
81 		       TAG_COMMENT, "comment_b2", TAG_ALBUM, "album_a2");
82 }
83 
84 static const char *uri1 = "/foo/bar.ogg";
85 static const char *uri2 = "foo/bar.ogg";
86 
87 DetachedSong
DatabaseDetachSong(const Database & db,const Storage * _storage,const char * uri)88 DatabaseDetachSong([[maybe_unused]] const Database &db,
89 		   [[maybe_unused]] const Storage *_storage,
90 		   const char *uri)
91 {
92 	if (strcmp(uri, uri2) == 0)
93 		return DetachedSong(uri, MakeTag2a());
94 
95 	throw std::runtime_error("No such song");
96 }
97 
98 bool
LoadFile(Path path)99 DetachedSong::LoadFile(Path path)
100 {
101 	if (path.ToUTF8() == uri1) {
102 		SetTag(MakeTag1a());
103 		return true;
104 	}
105 
106 	return false;
107 }
108 
109 const Database *
GetDatabase() const110 Client::GetDatabase() const noexcept
111 {
112 	return reinterpret_cast<const Database *>(this);
113 }
114 
115 const Storage *
GetStorage() const116 Client::GetStorage() const noexcept
117 {
118 	return ::storage;
119 }
120 
121 void
AllowFile(Path path_fs) const122 Client::AllowFile([[maybe_unused]] Path path_fs) const
123 {
124 	/* always fail, so a SongLoader with a non-nullptr
125 	   Client pointer will be regarded "insecure", while one with
126 	   client==nullptr will allow all files */
127 	throw std::runtime_error("foo");
128 }
129 
130 static std::string
ToString(const Tag & tag)131 ToString(const Tag &tag)
132 {
133 	std::string result;
134 
135 	if (!tag.duration.IsNegative())
136 		result.append(std::to_string(tag.duration.ToMS()));
137 
138 	for (const auto &item : tag) {
139 		result.push_back('|');
140 		result.append(tag_item_names[item.type]);
141 		result.push_back('=');
142 		result.append(item.value);
143 	}
144 
145 	return result;
146 }
147 
148 static std::string
ToString(const DetachedSong & song)149 ToString(const DetachedSong &song)
150 {
151 	std::string result = song.GetURI();
152 	result.push_back('|');
153 
154 	if (!IsNegative(song.GetLastModified()))
155 		result.append(std::to_string(std::chrono::system_clock::to_time_t(song.GetLastModified())));
156 
157 	result.push_back('|');
158 
159 	if (song.GetStartTime().IsPositive())
160 		result.append(std::to_string(song.GetStartTime().ToMS()));
161 
162 	result.push_back('-');
163 
164 	if (song.GetEndTime().IsPositive())
165 		result.append(std::to_string(song.GetEndTime().ToMS()));
166 
167 	result.push_back('|');
168 
169 	result.append(ToString(song.GetTag()));
170 
171 	return result;
172 }
173 
174 class TranslateSongTest : public ::testing::Test {
175 	std::unique_ptr<Storage> _storage;
176 
177 protected:
SetUp()178 	void SetUp() override {
179 		_storage = CreateLocalStorage(Path::FromFS(music_directory));
180 		storage = _storage.get();
181 	}
182 
TearDown()183 	void TearDown() override {
184 		_storage.reset();
185 	}
186 };
187 
TEST_F(TranslateSongTest,AbsoluteURI)188 TEST_F(TranslateSongTest, AbsoluteURI)
189 {
190 	DetachedSong song1("http://example.com/foo.ogg");
191 	auto se = ToString(song1);
192 	const SongLoader loader(nullptr, nullptr);
193 	EXPECT_TRUE(playlist_check_translate_song(song1, "/ignored",
194 						  loader));
195 	EXPECT_EQ(se, ToString(song1));
196 }
197 
TEST_F(TranslateSongTest,Insecure)198 TEST_F(TranslateSongTest, Insecure)
199 {
200 	/* illegal because secure=false */
201 	DetachedSong song1 (uri1);
202 	const SongLoader loader(*reinterpret_cast<const Client *>(1));
203 	EXPECT_FALSE(playlist_check_translate_song(song1, {},
204 						   loader));
205 }
206 
TEST_F(TranslateSongTest,Secure)207 TEST_F(TranslateSongTest, Secure)
208 {
209 	DetachedSong song1(uri1, MakeTag1b());
210 	auto se = ToString(DetachedSong(uri1, MakeTag1c()));
211 
212 	const SongLoader loader(nullptr, nullptr);
213 	EXPECT_TRUE(playlist_check_translate_song(song1, "/ignored",
214 						  loader));
215 	EXPECT_EQ(se, ToString(song1));
216 }
217 
TEST_F(TranslateSongTest,InDatabase)218 TEST_F(TranslateSongTest, InDatabase)
219 {
220 	const SongLoader loader(reinterpret_cast<const Database *>(1),
221 				storage);
222 
223 	DetachedSong song1("doesntexist");
224 	EXPECT_FALSE(playlist_check_translate_song(song1, {},
225 						   loader));
226 
227 	DetachedSong song2(uri2, MakeTag2b());
228 	auto se = ToString(DetachedSong(uri2, MakeTag2c()));
229 	EXPECT_TRUE(playlist_check_translate_song(song2, {},
230 						  loader));
231 	EXPECT_EQ(se, ToString(song2));
232 
233 	DetachedSong song3("/music/foo/bar.ogg", MakeTag2b());
234 	se = ToString(DetachedSong(uri2, MakeTag2c()));
235 	EXPECT_TRUE(playlist_check_translate_song(song3, {},
236 						  loader));
237 	EXPECT_EQ(se, ToString(song3));
238 }
239 
TEST_F(TranslateSongTest,Relative)240 TEST_F(TranslateSongTest, Relative)
241 {
242 	const Database &db = *reinterpret_cast<const Database *>(1);
243 	const SongLoader secure_loader(&db, storage);
244 	const SongLoader insecure_loader(*reinterpret_cast<const Client *>(1),
245 					 &db, storage);
246 
247 	/* map to music_directory */
248 	DetachedSong song1("bar.ogg", MakeTag2b());
249 	auto se = ToString(DetachedSong(uri2, MakeTag2c()));
250 	EXPECT_TRUE(playlist_check_translate_song(song1, "/music/foo",
251 						  insecure_loader));
252 	EXPECT_EQ(se, ToString(song1));
253 
254 	/* illegal because secure=false */
255 	DetachedSong song2("bar.ogg", MakeTag2b());
256 	EXPECT_FALSE(playlist_check_translate_song(song1, "/foo",
257 						   insecure_loader));
258 
259 	/* legal because secure=true */
260 	DetachedSong song3("bar.ogg", MakeTag1b());
261 	se = ToString(DetachedSong(uri1, MakeTag1c()));
262 	EXPECT_TRUE(playlist_check_translate_song(song3, "/foo",
263 						  secure_loader));
264 	EXPECT_EQ(se, ToString(song3));
265 
266 	/* relative to http:// */
267 	DetachedSong song4("bar.ogg", MakeTag2a());
268 	se = ToString(DetachedSong("http://example.com/foo/bar.ogg", MakeTag2a()));
269 	EXPECT_TRUE(playlist_check_translate_song(song4, "http://example.com/foo",
270 						  insecure_loader));
271 	EXPECT_EQ(se, ToString(song4));
272 }
273 
TEST_F(TranslateSongTest,Backslash)274 TEST_F(TranslateSongTest, Backslash)
275 {
276 	const SongLoader loader(reinterpret_cast<const Database *>(1),
277 				storage);
278 
279 	DetachedSong song1("foo\\bar.ogg", MakeTag2b());
280 #ifdef _WIN32
281 	/* on Windows, all backslashes are converted to slashes in
282 	   relative paths from playlists */
283 	auto se = ToString(DetachedSong(uri2, MakeTag2c()));
284 	EXPECT_TRUE(playlist_check_translate_song(song1, {},
285 						  loader));
286 	EXPECT_EQ(se, ToString(song1));
287 #else
288 	/* backslash only supported on Windows */
289 	EXPECT_FALSE(playlist_check_translate_song(song1, {},
290 						   loader));
291 #endif
292 }
293