[转]Git 中相对路径到绝对路径转换函数
相对路径到绝对路径的转换
给出一个相对路径,如何获得相应的绝对路径呢?在 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;
}
上面的代码反应出两个特征:
- 如果在 src 指向的字符串的前 n 个字节中都没有 '\0',那么 dest 中最终也不会有 '\0';
- 如果在 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 的实现中,路径转换的代码是十分简单直接的。思路是:
- 如果相对路径对应的不是一个目录,则先截取路径中的目录部分,并将 basename 部分保存在 last_elem 变量中;
- 通过 getcwd 获取当前工作目录,保存在 cwd 变量中,用于后面恢复;
- chdir 到上面所截取的相对路径目录上,然后 getcwd 获得该目录的绝对路径;
- 如果 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' 结尾的,所以要自己添加。
参考
- strlcpy 和 strlcat:http://en.wikipedia.org/wiki/Strlcpy

浙公网安备 33010602011771号