[转]Git 中相对路径到绝对路径转换函数

转:http://gonggeng.org/mediawiki/index.php/Git_%E4%B8%AD%E7%9B%B8%E5%AF%B9%E8%B7%AF%E5%BE%84%E5%88%B0%E7%BB%9D%E5%AF%B9%E8%B7%AF%E5%BE%84%E8%BD%AC%E6%8D%A2%E5%87%BD%E6%95%B0

相对路径到绝对路径的转换

给出一个相对路径,如何获得相应的绝对路径呢?在 git 中,是通过 make_absolute_path() 函数实现的。

#define MAXDEPTH 5
 
const char *make_absolute_path(const char *path)
{
	static char bufs[2][PATH_MAX + 1], *buf = bufs[0], *next_buf = bufs[1];
	char cwd[1024] = "";
	int buf_index = 1, len;
 
	int depth = MAXDEPTH;
	char *last_elem = NULL;
	struct stat st;
 
	if (strlcpy(buf, path, PATH_MAX) >= PATH_MAX)
		die ("Too long path: %.*s", 60, path);
 
	while (depth--) {
		if (stat(buf, &st) || !S_ISDIR(st.st_mode)) {
			char *last_slash = strrchr(buf, '/');
			if (last_slash) {
				*last_slash = '\0';
				last_elem = xstrdup(last_slash + 1);
			} else {
				last_elem = xstrdup(buf);
				*buf = '\0';
			}
		}
 
		if (*buf) {
			if (!*cwd && !getcwd(cwd, sizeof(cwd)))
				die ("Could not get current working directory");
 
			if (chdir(buf))
				die ("Could not switch to '%s'", buf);
		}
		if (!getcwd(buf, PATH_MAX))
			die ("Could not get current working directory");
 
		if (last_elem) {
			int len = strlen(buf);
			if (len + strlen(last_elem) + 2 > PATH_MAX)
				die ("Too long path name: '%s/%s'",
						buf, last_elem);
			buf[len] = '/';
			strcpy(buf + len + 1, last_elem);
			free(last_elem);
			last_elem = NULL;
		}
 
		if (!lstat(buf, &st) && S_ISLNK(st.st_mode)) {
			len = readlink(buf, next_buf, PATH_MAX);
			if (len < 0)
				die ("Invalid symlink: %s", buf);
			next_buf[len] = '\0';
			buf = next_buf;
			buf_index = 1 - buf_index;
			next_buf = bufs[buf_index];
		} else
			break;
	}
 
	if (*cwd && chdir(cwd))
		die ("Could not change back to '%s'", cwd);
 
	return buf;
}

代码分析

这个函数首先在栈上分配两个长度为 PATH_MAX+1 的 char 数组,PATH_MAX 又是多少呢?PATH_MAX 这个宏在 /usr/include/linux/limits.h 里面定义,

#define PATH_MAX        4096	/* # chars in a path name including nul */

这样做虽然会浪费一些空间,但是可以简化后面的代码,因为可以保证没有任何路径长度会超过这个值。

接下来函数将输入的相对路径拷贝到其中一个数组 buf 里面,这里调用了一个比较“陌生”的函数:strlcpy,我们先来调查一下这个函数的来历。

strlcpy 与 strncpy

如果你查 man strlcpy 的话,恐怕会失望,因为在 Linux 中没有这个函数,这个函数也不是 glibc 提供的,不是标准C语言的一部分。至于为什么在这里用 strlcpy 而不用 strncpy,我们先看看 strncpy 有什么固有的限制。

通过 man strncpy,可以发现,strncpy 的使用其实有一些需要注意的地方。它的原型是

char *strncpy(char *dest, const char *src, size_t n);

其语义相当于以下代码

           char*
           strncpy(char *dest, const char *src, size_t n){
               size_t i;

               for (i = 0 ; i < n && src[i] != '\0' ; i++)
                   dest[i] = src[i];
               for ( ; i < n ; i++)
                   dest[i] = '\0';

               return dest;
           }

上面的代码反应出两个特征:

  1. 如果在 src 指向的字符串的前 n 个字节中都没有 '\0',那么 dest 中最终也不会有 '\0';
  2. 如果在 src 指向的字符串中,第一个 '\0' 之前的字节数小于 n 的话,从这个 '\0' 的位置开始到第 n 个字节的数据都会被填充为 '\0'。

对程序员来说,前一个特征往往会导致潜在的 bug,因为 dest 指向的字符串可能没有恰当的结尾 '\0';后一个特征则代表潜在的低效率,因为剩下的字节其实都是没用的,填充它们很多时候是浪费时间。

针对上述的第一个问题,很多程序员会用与下面代码类似的方法来使用 strncpy

           strncpy(buf, str, n);
           if (n > 0)
               buf[n - 1]= '\0';

上面的代码也无法避免 strncpy 潜在的性能问题,而且程序员容易漏掉后面的“加尾”操作。除此之外,strncpy 的另一个不足之处是,程序员无法从 strncpy 的返回值中获知 src 字符串是否被“截短复制”(也就是字符串长度大于 n),这种“截短”往往是程序员所不希望的,如果出现了,应该作为一个错误报告。在这种情况下,程序员只能自己调用 strlen,比较字符串长度和 n 的大小。

针对上面的几个问题,OpenBSD 首先引入了 strlcpy 函数,其原型为

size_t strlcpy(char *dest, const char *src, size_t size);

它的语义是:

  • 最多复制 size-1 个字节,并且保证 dest 的末尾为 '\0',总共 size 个字节;
  • 如果 src 的长度不足 size-1,那么剩余的字节不会被填充为 '\0';
  • 函数返回 src 字符串的长度,程序员可以将这个返回值与 size 比较以判断是否有“截短”。

这个函数现在在 FreeBSD, Solaris 和 Max OS X 中都有实现,但是 Linux 的 GNU Lib C 并没有采用这个函数,主要原因是 glibc 的开发者认为 str*cpy 函数本身就不值得提倡,程序员应该自己清楚复制的字符串到底有多长,然后分配足够的空间,最后使用高效的 memcpy 函数实现复制,而不是依赖于 str*cpy 函数的不一致的语义。事实上,在 git 的代码里面自己也实现了 strlcpy 函数,用的思路与 glibc 开发者指出的类似,只不过是封装了一下

size_t gitstrlcpy(char *dest, const char *src, size_t size)
{
	size_t ret = strlen(src);

	if (size) {
		size_t len = (ret >= size) ? size - 1 : ret;
		memcpy(dest, src, len);
		dest[len] = '\0';
	}
	return ret;
}

路径转换的主体代码

在 git 的实现中,路径转换的代码是十分简单直接的。思路是:

  1. 如果相对路径对应的不是一个目录,则先截取路径中的目录部分,并将 basename 部分保存在 last_elem 变量中;
  2. 通过 getcwd 获取当前工作目录,保存在 cwd 变量中,用于后面恢复;
  3. chdir 到上面所截取的相对路径目录上,然后 getcwd 获得该目录的绝对路径;
  4. 如果 last_elem 非空,也就是相对路径对应的不是目录的话,还需要将上一步获得的目录的绝对路径和 last_elem 连接起来成为一个完整的路径。

这种实现虽然简单,但是有一些隐含的问题:

  • 使用 chdir,由内核完成相对路径到绝对路径的转换,虽然简单,但是不是可重入的实现方法;
  • 由于 chdir 是跟随符号连接的,所以如果输入的相对路径是一个到目录的符号连接的话,chdir 会直接转到连接的目标目录上,因而后面的 getcwd 也会返回连接目标目录的绝对路径,而不是符号连接文件本身的绝对路径。

这里可以看到将 buf 的大小定为 PATH_MAX 的好处,这样在调用 getcwd 的时候就不用考虑数组不够长度的出错情况了。

符号连接的处理

在 Linux 中,几乎所有的系统调用都是跟随符号连接的,也就是作用在符号连接的目标上。只有少数几个 系统调用不跟随符号连接,如 lstat, readlink(更详细的可以看 man 7 symlink)。

chdir 是一个跟随符号连接的系统调用,也就是说,如果参数文件是一个符号连接,而且连接的目标 是一个目录的话,chdir 就会将进程当前工作目录转到连接的目标目录上。因此,为了保持函数语义上 的一致性,如果 make_absolute_path 的参数是一个到文件的符号连接的话,也应该要跟随这个符号 连接,也就是说返回的应该是符号连接目标文件的绝对路径。但是,并没有现成的系统调用可以完成 这个功能,必须自己来实现。

由于符号连接可能存在无限循环引用,所以必须限制跟随的层次,如果层次超过一定的值就要返回错误。 代码中具体的做法是,使用 readlink 系统调用获得符号连接的目标路径,这算是一次跟随, 如果获得的路径也是符号连接,那就需要循环处理,循环的次数不超过5次。 这里需要注意 readlink 系统调用返回的路径字符串是不加 '\0' 结尾的,所以要自己添加。

参考

posted @ 2012-01-31 16:06  杨军  阅读(1026)  评论(0)    收藏  举报