今天遇到的案例,一個.NET 2.0開發的FTP上傳程式在A機器運作正常,移到B機器執行卻出現路徑錯誤:

string ftpUrl = "ftp://blah.boo.boo.blahboo//folder/file.txt";
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUrl);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = new NetworkCredential(uname, upass);
byte[] fileContents = Encoding.UTF8.GetBytes("Test");
request.ContentLength = fileContents.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(fileContents, 0, fileContents.Length);
requestStream.Close();
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Console.WriteLine("Result={0}", response.StatusDescription);
response.Close();

很明顯地,這裡面有上回提過的WebClient具名登入FTP的絕對路徑問題,依前次的結論,用"//"或"%2F"表示絕對目錄都是可行的。但在本案例,在B機器執行必須改成”%2F/folder/file.txt”才會成功,機器A則兩者都行。(補充: MSDN文件關於FtpWebRequest的說明中只提到用%2f表示絕對路徑,並未提及"//"寫法,故建議使用%2f較無疑義。)

接著,開始追查為什麼兩台機器對FTP Uri的解析會有不同的結果?

使用反組譯工具深入System.dll原始碼,發現FtpWebRequest會依賴FtpControlStream.GetPathAndFileName內部方法解析路徑,而在其中則是透過內部方法Uri.GetParts(UriComponents.Path | UriComponents.KeepDelimiter, UriFormat.Unescaped )由"ftp://blah/folder/file"擷取路徑,再由路徑字串是否以"//"起始判斷絕對路徑或相對路徑。由此推斷,這問題有可能是兩台機器的Uri.GetParts()對"//"寫法的處理結果不同所致。

最後我整理出可以重現差異的程式片段: (可使用Mini C# Lab執行)

using System;
using System.IO;
using System.Threading;
using System.Net;
using System.Text;
using System.Reflection;
 
public class CSharpLab
{
    public static void Test()
    {
        Type uriType = typeof(Uri);
        MethodInfo mi = uriType.GetMethod("GetParts", 
                        BindingFlags.Instance | BindingFlags.NonPublic);
        object[] args = new object[] { 
                        UriComponents.Path | UriComponents.KeepDelimiter,
                        UriFormat.Unescaped 
                        };
        string str = mi.Invoke(
                new Uri("ftp://testserver//etc/config.txt"), args).ToString();
        Console.WriteLine(str);
        str = mi.Invoke(
                new Uri("ftp://testserver/%2fetc/config.txt"), args).ToString();
        Console.WriteLine(str);
    }
}

程式中用Reflection的技巧叫Uri的內部方法GetParts(),分別測試"//etc/config.txt”及"/%2etc/fconfig.txt兩個Uri的路徑解析結果。在機器A執行結果為:

//etc/config.txt
//etc/config.txt

在機器B執行結果卻是:

/etc/config.txt
//etc/config.txt

Bingo!!!

測試結果驗證了"//etc/config.txt”及"/%2fect/config.txt”在機器A上都會被當成絕對路徑,但在機器B"ftp://testserver//etc/config.txt"視同"ftp://testserver/etc/config.txt",必須寫成"/%2f”才算絕對路徑,至此,在同一程式在機器B執行會出現路徑錯誤的謎團已獲得解答。

但下一個問題是: 同樣是.NET 2.0 Runtime,莫非還有版本區別,導致行為差異? 再深入追查,發覺機器A有安裝.NET 3.5,而機器B只裝了.NET 2.0。這才發現一個先前被我忽略的祕密 --- 安裝.NET 3.5時會偷偷升級.NET 2.0 Runtime版本。機器B的System.dll版本是2.0.50727.42,而機器A在安裝.NET 3.5後System.dll的版號變成2.0.50727.3624,換句話說,Uri.GetParts()在2.0.50727.42版本只接受"/%2f"寫法,到了2.0.50727.3624變成"//"及"/%2f"寫法都通。

過去,我一直以為.NET 3.5骨子裡基於.NET 2.0 Runtime,只透過外加System.*.dll、Extension Method等做法增加LINQ等新功能,沒意識到.NET 2.0 Runtime底層也被做了一些更動調整,而.NET 2.0部分元件行為在升級.NET 3.5後改變更是出乎我意料。(另外,實測發現安裝.NET 3.0時還不會變動.NET 2.0版本,安裝.NET 3.5後才會更新)

今天學到的心得:

.NET 2.0 Runtime在安裝.NET 3.5後版本會被更新,有少部分元件的行為可能因此改變。


Comments

Be the first to post a comment

Post a comment