You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让本地文件SHA1哈希与GitHub API返回值匹配?

本地文件SHA1哈希与GitHub API返回值不匹配问题

我正在尝试将本地文件的哈希值与GitHub API返回的哈希值对比:

  • 创建了test.txt并上传到仓库,调用compute_sha1_hash函数得到的哈希值为:2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
  • GitHub API响应中该文件的哈希值为:95d09f2b10159347eece71399a7e2e907ea3df4f

已知GitHub使用SHA1算法,为何两者不匹配?是否需要额外计算才能对应?我已经实现了添加blob 前缀加文件长度加空字节的方案(compute_sha1_hash_2函数),但哈希值还是不匹配。

完整代码如下:

#include <curl/curl.h>
#include <openssl/evp.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <iomanip>
#include <iostream>
#include <regex>

std::string compute_sha1_hash(const std::string& file_path)
{
    std::ifstream file(file_path, std::ios::binary);
    if (!file) {
        throw std::runtime_error("Could not open file");
    }

    EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
    if (mdctx == NULL) {
        throw std::runtime_error("Failed to create EVP_MD_CTX");
    }

    if (EVP_DigestInit_ex(mdctx, EVP_sha1(), NULL) != 1) {
        throw std::runtime_error("Failed to initialize SHA-1 context");
    }

    std::vector<char> buffer(4096);
    while (file.read(buffer.data(), buffer.size())) {
        if (EVP_DigestUpdate(mdctx, buffer.data(), file.gcount()) != 1) {
            throw std::runtime_error("Failed to update SHA-1 context");
        }
    }

    if (file.gcount() > 0) {
        if (EVP_DigestUpdate(mdctx, buffer.data(), file.gcount()) != 1) {
            throw std::runtime_error("Failed to update SHA-1 context");
        }
    }

    std::vector<unsigned char> hash_value(EVP_MD_size(EVP_sha1()));
    unsigned int digest_len;
    if (EVP_DigestFinal_ex(mdctx, hash_value.data(), &digest_len) != 1) {
        throw std::runtime_error("Failed to finalize SHA-1 context");
    }

    EVP_MD_CTX_free(mdctx);

    // Convert to hex
    std::ostringstream oss;
    for (const auto& byte : hash_value) {
        oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
    }

    return oss.str();
}

// Brian613542705's answer
std::string compute_sha1_hash_2(const std::string& file_path)
{
   std::ifstream file(file_path, std::ios::binary);
   if (!file) {
      throw std::runtime_error("Could not open file");
   }

   // Read the content of the file
   std::ostringstream oss;
   oss << file.rdbuf();
   std::string content = oss.str();

   // Prepend "blob " and the size of the content
   std::string preparedContent = "blob " + std::to_string(content.size()) + "\0" + content;

   EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
   if (mdctx == NULL) {
      throw std::runtime_error("Failed to create EVP_MD_CTX");
   }

   if (EVP_DigestInit_ex(mdctx, EVP_sha1(), NULL) != 1) {
      throw std::runtime_error("Failed to initialize SHA-1 context");
   }

   if (EVP_DigestUpdate(mdctx, preparedContent.c_str(), preparedContent.size()) != 1) {
      throw std::runtime_error("Failed to update SHA-1 context");
   }

   std::vector<unsigned char> hash_value(EVP_MD_size(EVP_sha1()));
   unsigned int digest_len;
   if (EVP_DigestFinal_ex(mdctx, hash_value.data(), &digest_len) != 1) {
      throw std::runtime_error("Failed to finalize SHA-1 context");
   }

   EVP_MD_CTX_free(mdctx);

   // Convert to hex
   std::ostringstream sha1oss;
   for (const auto& byte : hash_value) {
      sha1oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
   }

   return sha1oss.str();
}

 static size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb, void* userp) 
 {
     size_t realsize = size * nmemb;
     auto& mem = *static_cast<std::string*>(userp);
     mem.append(static_cast<char*>(contents), realsize);
     return realsize;
 }

void curl(std::string& data, const char* url)
{
    CURL* curl_handle = curl_easy_init();
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);
    curl_easy_setopt(curl_handle, CURLOPT_TCP_KEEPALIVE, 0);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &data);
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
    curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L);  // redirects
    CURLcode res = curl_easy_perform(curl_handle);
    if(res != CURLE_OK)
        std::cerr << "curl_easy_perform failed: " << curl_easy_strerror(res) << '\n';
    curl_easy_cleanup(curl_handle);
    curl_global_cleanup();
}

int main() 
{
    std::string fileHash = compute_sha1_hash("C:/Users/jabu/test.txt");
    std::string fileHash_2 = compute_sha1_hash_2("C:/Users/jabu/test.txt");
    std::string jsonString;
    curl(jsonString, "https://api.github.com/repos/jajabu33/test/git/trees/main?recursive=1");

    // just to kick compare the strings
    std::regex shaPattern("\"sha\":\\s*\"([a-fA-F0-9]+)\"");
    std::sregex_iterator shaIterator(jsonString.begin(), jsonString.end(), shaPattern);
    std::sregex_iterator endIterator;

    std::string githubHash;
    for (; shaIterator != endIterator; ++shaIterator)
    {
        std::smatch match = *shaIterator;
        if (match.size() >= 2)
            githubHash = match[1].str();
    }

    if (fileHash == githubHash)
        std::cout << "Hashes are equal\n";
    else
        std::cout << "Hashes are not equal\n";
    return 0;
}

问题原因与解决方案

核心原因:换行符差异或文件读取逻辑问题

GitHub默认会将Windows格式的CRLF换行符转换为LF存储,若本地文件是CRLF格式,两者字节流内容和长度不一致,即使使用blob前缀计算哈希也无法匹配。另外,原compute_sha1_hash_2用ostringstream读取二进制文件可能存在隐藏的编码或字节处理问题。

解决步骤:

  1. 统一文件换行符格式
    将本地文件转换为LF格式(与GitHub存储的一致),可通过VS Code等编辑器将换行符设置为LF后保存。

  2. 修正文件读取与哈希计算逻辑
    改用二进制方式直接读取文件字节,避免ostringstream可能带来的字节处理偏差:

    std::string compute_sha1_hash_2(const std::string& file_path)
    {
        std::ifstream file(file_path, std::ios::binary | std::ios::ate);
        if (!file) {
            throw std::runtime_error("Could not open file");
        }
    
        std::streamsize size = file.tellg();
        file.seekg(0, std::ios::beg);
    
        std::vector<char> buffer(size);
        if (!file.read(buffer.data(), size)) {
            throw std::runtime_error("Failed to read file");
        }
    
        // 构建GitHub要求的blob前缀格式
        std::string header = "blob " + std::to_string(size) + "\0";
        std::string preparedContent;
        preparedContent.reserve(header.size() + size);
        preparedContent.append(header);
        preparedContent.append(buffer.data(), size);
    
        // 计算SHA1
        EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
        if (mdctx == NULL) {
            throw std::runtime_error("Failed to create EVP_MD_CTX");
        }
    
        if (EVP_DigestInit_ex(mdctx, EVP_sha1(), NULL) != 1) {
            throw std::runtime_error("Failed to initialize SHA-1 context");
        }
    
        if (EVP_DigestUpdate(mdctx, preparedContent.c_str(), preparedContent.size()) != 1) {
            throw std::runtime_error("Failed to update SHA-1 context");
        }
    
        std::vector<unsigned char> hash_value(EVP_MD_size(EVP_sha1()));
        unsigned int digest_len;
        if (EVP_DigestFinal_ex(mdctx, hash_value.data(), &digest_len) != 1) {
            throw std::runtime_error("Failed to finalize SHA-1 context");
        }
    
        EVP_MD_CTX_free(mdctx);
    
        // 转换为十六进制字符串
        std::ostringstream sha1oss;
        sha1oss << std::hex << std::setfill('0');
        for (const auto& byte : hash_value) {
            sha1oss << std::setw(2) << static_cast<int>(byte);
        }
    
        return sha1oss.str();
    }
    
  3. 精准提取目标文件的哈希值
    原代码用正则匹配所有SHA1值,最后获取的可能不是test.txt的哈希。建议使用JSON库解析API响应,精准定位目标文件:

    // 引入nlohmann/json库
    #include <nlohmann/json.hpp>
    using json = nlohmann::json;
    
    // 在main函数中替换哈希提取逻辑
    json parsedJson = json::parse(jsonString);
    std::string githubHash;
    for (const auto& item : parsedJson["tree"]) {
        if (item["path"] == "test.txt") {
            githubHash = item["sha"];
            break;
        }
    }
    
  4. 验证文件字节一致性
    通过GitHub API获取文件原始内容,保存后对比本地文件的字节流,确保两者完全一致。

验证方法

修改后重新运行代码:

  • 确认本地文件与GitHub存储的文件字节流完全匹配(换行符、空字节等)
  • 使用修正后的compute_sha1_hash_2计算本地哈希
  • 精准提取test.txt对应的GitHub哈希
  • 对比两者是否一致

内容的提问来源于stack exchange,提问作者Jabu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 02:35:56