TestEtcHostsParsing.cpp (9726B)
1 #include "gtest/gtest.h" 2 3 #include "mozilla/net/rust_helper.h" 4 #include "nsDirectoryServiceDefs.h" 5 #include "nsDirectoryServiceUtils.h" 6 #include "nsIFile.h" 7 #include "nsIFileStreams.h" 8 #include "nsNetUtil.h" 9 #include "nsString.h" 10 #include "nsTArray.h" 11 #include "nsUnicharUtils.h" 12 #include <fcntl.h> 13 #include <sys/stat.h> 14 15 #ifndef XP_WIN 16 # include <sys/file.h> 17 # include <unistd.h> 18 #endif 19 20 #ifdef XP_WIN 21 # include <io.h> 22 # include <share.h> 23 #endif 24 25 using namespace mozilla; 26 using namespace mozilla::net; 27 28 class TestEtcHostsParsing : public ::testing::Test { 29 protected: 30 void SetUp() override { 31 // Get temp directory 32 nsCOMPtr<nsIFile> tmpDir; 33 nsresult rv = 34 NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir)); 35 ASSERT_TRUE(NS_SUCCEEDED(rv)); 36 37 // Create unique test file names 38 nsAutoString baseName(u"test_hosts_"); 39 baseName.AppendInt(PR_IntervalNow()); 40 41 // Normal test file 42 rv = tmpDir->Clone(getter_AddRefs(mTestHostsFile)); 43 ASSERT_TRUE(NS_SUCCEEDED(rv)); 44 nsAutoString normalName = baseName + u"_normal"_ns; 45 rv = mTestHostsFile->Append(normalName); 46 ASSERT_TRUE(NS_SUCCEEDED(rv)); 47 48 // Locked test file 49 rv = tmpDir->Clone(getter_AddRefs(mLockedHostsFile)); 50 ASSERT_TRUE(NS_SUCCEEDED(rv)); 51 nsAutoString lockedName = baseName + u"_locked"_ns; 52 rv = mLockedHostsFile->Append(lockedName); 53 ASSERT_TRUE(NS_SUCCEEDED(rv)); 54 55 // Permission denied test file 56 rv = tmpDir->Clone(getter_AddRefs(mNoPermHostsFile)); 57 ASSERT_TRUE(NS_SUCCEEDED(rv)); 58 nsAutoString noPermName = baseName + u"_noperm"_ns; 59 rv = mNoPermHostsFile->Append(noPermName); 60 ASSERT_TRUE(NS_SUCCEEDED(rv)); 61 62 mCallbackInvoked = false; 63 mCallbackResult = true; 64 mParsedHosts.Clear(); 65 } 66 67 void TearDown() override { 68 // Clean up test files 69 if (mTestHostsFile) { 70 mTestHostsFile->Remove(false); 71 } 72 if (mLockedHostsFile) { 73 mLockedHostsFile->Remove(false); 74 } 75 if (mNoPermHostsFile) { 76 mNoPermHostsFile->Remove(false); 77 } 78 79 // Close any open file descriptors 80 if (mLockFd != -1) { 81 #ifdef XP_WIN 82 _close(mLockFd); 83 #else 84 close(mLockFd); 85 #endif 86 mLockFd = -1; 87 } 88 } 89 90 void CreateNormalHostsFile() { 91 nsCOMPtr<nsIOutputStream> stream; 92 nsresult rv = 93 NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTestHostsFile); 94 ASSERT_TRUE(NS_SUCCEEDED(rv)); 95 96 const char* content = 97 "127.0.0.1 localhost\n" 98 "::1 localhost\n" 99 "127.0.0.1 example.com www.example.com\n" 100 "# This is a comment\n" 101 "192.168.1.1 router.local\n"; 102 103 uint32_t written; 104 rv = stream->Write(content, strlen(content), &written); 105 ASSERT_TRUE(NS_SUCCEEDED(rv)); 106 ASSERT_EQ(written, strlen(content)); 107 108 rv = stream->Close(); 109 ASSERT_TRUE(NS_SUCCEEDED(rv)); 110 } 111 112 void CreateLockedHostsFile() { 113 // First create the file with content 114 nsCOMPtr<nsIOutputStream> stream; 115 nsresult rv = 116 NS_NewLocalFileOutputStream(getter_AddRefs(stream), mLockedHostsFile); 117 ASSERT_TRUE(NS_SUCCEEDED(rv)); 118 119 const char* content = "127.0.0.1 locked.test\n"; 120 uint32_t written; 121 rv = stream->Write(content, strlen(content), &written); 122 ASSERT_TRUE(NS_SUCCEEDED(rv)); 123 rv = stream->Close(); 124 ASSERT_TRUE(NS_SUCCEEDED(rv)); 125 126 // Now lock it by opening with exclusive access 127 nsAutoCString nativePath; 128 rv = GetCrossplatformNativePath(mLockedHostsFile, nativePath); 129 ASSERT_TRUE(NS_SUCCEEDED(rv)); 130 131 #ifdef XP_WIN 132 // On Windows, open with no sharing to simulate a locked file 133 mLockFd = _sopen(nativePath.get(), _O_RDONLY, _SH_DENYRW, _S_IREAD); 134 #else 135 // On Unix, use flock to lock the file 136 mLockFd = open(nativePath.get(), O_RDONLY); 137 if (mLockFd != -1) { 138 ::flock(mLockFd, LOCK_EX | LOCK_NB); // Exclusive, non-blocking lock 139 } 140 #endif 141 ASSERT_NE(mLockFd, -1); 142 } 143 144 void CreateNoPermissionFile() { 145 #ifndef XP_WIN 146 // Create the file first 147 nsCOMPtr<nsIOutputStream> stream; 148 nsresult rv = 149 NS_NewLocalFileOutputStream(getter_AddRefs(stream), mNoPermHostsFile); 150 ASSERT_TRUE(NS_SUCCEEDED(rv)); 151 152 const char* content = "127.0.0.1 noperm.test\n"; 153 uint32_t written; 154 rv = stream->Write(content, strlen(content), &written); 155 ASSERT_TRUE(NS_SUCCEEDED(rv)); 156 rv = stream->Close(); 157 ASSERT_TRUE(NS_SUCCEEDED(rv)); 158 159 // Remove read permissions 160 nsAutoCString nativePath; 161 rv = GetCrossplatformNativePath(mNoPermHostsFile, nativePath); 162 ASSERT_TRUE(NS_SUCCEEDED(rv)); 163 chmod(nativePath.get(), 0000); // No permissions 164 #endif 165 } 166 167 nsresult GetCrossplatformNativePath(nsIFile* aFile, nsACString& aPath) { 168 #ifdef XP_WIN 169 nsAutoString widePath; 170 nsresult rv = aFile->GetPath(widePath); 171 if (NS_FAILED(rv)) return rv; 172 aPath = NS_ConvertUTF16toUTF8(widePath); 173 return NS_OK; 174 #else 175 return aFile->GetNativePath(aPath); 176 #endif 177 } 178 179 static bool TestCallback(const nsTArray<nsCString>* aArray) { 180 auto* self = static_cast<TestEtcHostsParsing*>(sCurrentTest); 181 self->mCallbackInvoked = true; 182 183 if (aArray) { 184 self->mParsedHosts.AppendElements(*aArray); 185 } 186 187 return self->mCallbackResult; 188 } 189 190 nsCOMPtr<nsIFile> mTestHostsFile; 191 nsCOMPtr<nsIFile> mLockedHostsFile; 192 nsCOMPtr<nsIFile> mNoPermHostsFile; 193 int mLockFd = -1; 194 195 bool mCallbackInvoked; 196 bool mCallbackResult; 197 nsTArray<nsCString> mParsedHosts; 198 199 static TestEtcHostsParsing* sCurrentTest; 200 }; 201 202 TestEtcHostsParsing* TestEtcHostsParsing::sCurrentTest = nullptr; 203 204 TEST_F(TestEtcHostsParsing, ParseNormalFile) { 205 sCurrentTest = this; 206 CreateNormalHostsFile(); 207 208 nsAutoCString path; 209 nsresult rv = GetCrossplatformNativePath(mTestHostsFile, path); 210 ASSERT_TRUE(NS_SUCCEEDED(rv)); 211 212 rust_parse_etc_hosts(&path, TestCallback); 213 214 EXPECT_TRUE(mCallbackInvoked); 215 EXPECT_GT(mParsedHosts.Length(), 0u); 216 217 // Check that expected hostnames are parsed 218 bool foundLocalhost = false; 219 bool foundExampleCom = false; 220 bool foundWwwExampleCom = false; 221 bool foundRouterLocal = false; 222 223 for (const auto& host : mParsedHosts) { 224 if (host.EqualsLiteral("localhost")) foundLocalhost = true; 225 if (host.EqualsLiteral("example.com")) foundExampleCom = true; 226 if (host.EqualsLiteral("www.example.com")) foundWwwExampleCom = true; 227 if (host.EqualsLiteral("router.local")) foundRouterLocal = true; 228 } 229 230 EXPECT_TRUE(foundLocalhost); 231 EXPECT_TRUE(foundExampleCom); 232 EXPECT_TRUE(foundWwwExampleCom); 233 EXPECT_TRUE(foundRouterLocal); 234 } 235 236 TEST_F(TestEtcHostsParsing, ParseLockedFile) { 237 sCurrentTest = this; 238 CreateLockedHostsFile(); 239 240 nsAutoCString path; 241 nsresult rv = GetCrossplatformNativePath(mLockedHostsFile, path); 242 ASSERT_TRUE(NS_SUCCEEDED(rv)); 243 244 // The function should handle locked files gracefully and not crash or hang 245 rust_parse_etc_hosts(&path, TestCallback); 246 247 // On some systems, locked files might still be readable, on others not. 248 // The important thing is that the function doesn't crash or hang. 249 // We don't assert on mCallbackInvoked because it depends on platform 250 // behavior. 251 } 252 253 TEST_F(TestEtcHostsParsing, ParseNoPermissionFile) { 254 #ifndef XP_WIN 255 sCurrentTest = this; 256 CreateNoPermissionFile(); 257 258 nsAutoCString path; 259 nsresult rv = GetCrossplatformNativePath(mNoPermHostsFile, path); 260 ASSERT_TRUE(NS_SUCCEEDED(rv)); 261 262 // The function should handle permission denied gracefully 263 rust_parse_etc_hosts(&path, TestCallback); 264 265 // Callback should not be invoked for files we can't read 266 EXPECT_FALSE(mCallbackInvoked); 267 #endif 268 } 269 270 TEST_F(TestEtcHostsParsing, ParseNonExistentFile) { 271 sCurrentTest = this; 272 273 nsAutoCString fakePath("/nonexistent/file/hosts"); 274 275 // The function should handle non-existent files gracefully 276 rust_parse_etc_hosts(&fakePath, TestCallback); 277 278 // Callback should not be invoked for non-existent files 279 EXPECT_FALSE(mCallbackInvoked); 280 } 281 282 TEST_F(TestEtcHostsParsing, CallbackReturnsFalse) { 283 sCurrentTest = this; 284 CreateNormalHostsFile(); 285 mCallbackResult = false; // Tell callback to return false 286 287 nsAutoCString path; 288 nsresult rv = GetCrossplatformNativePath(mTestHostsFile, path); 289 ASSERT_TRUE(NS_SUCCEEDED(rv)); 290 291 rust_parse_etc_hosts(&path, TestCallback); 292 293 EXPECT_TRUE(mCallbackInvoked); 294 // When callback returns false, parsing should stop early 295 // So we shouldn't get all the hosts 296 } 297 298 TEST_F(TestEtcHostsParsing, EmptyFile) { 299 sCurrentTest = this; 300 301 // Create empty file 302 nsCOMPtr<nsIOutputStream> stream; 303 nsresult rv = 304 NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTestHostsFile); 305 ASSERT_TRUE(NS_SUCCEEDED(rv)); 306 rv = stream->Close(); 307 ASSERT_TRUE(NS_SUCCEEDED(rv)); 308 309 nsAutoCString path; 310 rv = GetCrossplatformNativePath(mTestHostsFile, path); 311 ASSERT_TRUE(NS_SUCCEEDED(rv)); 312 313 rust_parse_etc_hosts(&path, TestCallback); 314 315 // Callback might or might not be invoked for empty files, but shouldn't crash 316 EXPECT_EQ(mParsedHosts.Length(), 0u); 317 } 318 319 TEST_F(TestEtcHostsParsing, FileWithOnlyComments) { 320 sCurrentTest = this; 321 322 nsCOMPtr<nsIOutputStream> stream; 323 nsresult rv = 324 NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTestHostsFile); 325 ASSERT_TRUE(NS_SUCCEEDED(rv)); 326 327 const char* content = 328 "# This is a comment\n" 329 "# Another comment\n" 330 " # Indented comment\n"; 331 332 uint32_t written; 333 rv = stream->Write(content, strlen(content), &written); 334 ASSERT_TRUE(NS_SUCCEEDED(rv)); 335 rv = stream->Close(); 336 ASSERT_TRUE(NS_SUCCEEDED(rv)); 337 338 nsAutoCString path; 339 rv = GetCrossplatformNativePath(mTestHostsFile, path); 340 ASSERT_TRUE(NS_SUCCEEDED(rv)); 341 342 rust_parse_etc_hosts(&path, TestCallback); 343 344 // Should not parse any hosts from a file with only comments 345 EXPECT_EQ(mParsedHosts.Length(), 0u); 346 }