GithubHelp home page GithubHelp logo

copilot-analysis-new's Introduction

再次揭秘Copilot:sourcemap逆向分析

源码详细分析

背景

今年五月的时候我写了一篇文章《花了大半个月,我终于逆向分析了Github Copilot》,最近发现copilot.map文件也提交了上来,**sourcemap**中包含了整体源码的结构信息和部分变量信息,这无疑为分析copilot源码带来了极大的便利,因此再次分析一波。

sourcemap

sourcemap是什么

Sourcemap 是一种用于将编译、打包后的代码映射回原始源代码的技术。它主要用于 JavaScript 的源代码映射(source map),但也可以用于其他编程语言。

在 JavaScript 中,源代码映射(source map)是一种文件,它允许浏览器将压缩、混淆或转译后的代码映射回原始源代码。这对于调试非常有用,因为它允许开发者查看和调试原始源代码,而不是被压缩或混淆的代码。

Sourcemap 文件通常以 .map 扩展名结尾,并且可以通过浏览器的开发者工具查看和使用。

sourcemap的结构

Sourcemap 文件的结构主要包括以下几个部分:

  1. Version: 这是源映射文件的版本号。目前,Sourcemap 的版本号为 3。
  2. File: 这是源映射文件所对应的原始源文件的名称。
  3. SourceRoot: 这是一个可选的字段,它指定了源文件的根路径。
  4. Sources: 这是一个包含所有原始源文件名的数组。
  5. Names: 这是一个包含所有原始源文件中使用的变量、函数和类的名称的数组。
  6. Mappings: 这是一个字符串,它描述了源文件和生成文件之间的映射关系。

下面是一个简单的 Sourcemap 文件的示例:

{
  "version": 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "bar.js"],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "AAAA,SAASA,EAAM,CAAE,GAAG,EAAE,CACC,CAAC"
}

在这个示例中,mappings 字段描述了源文件和生成文件之间的映射关系。这个字符串被分成多个部分,每个部分对应源文件的一行。每个部分由一系列的映射组成,每个映射描述了源文件中的一个字符在生成文件中的位置。

mappings的含义

Sourcemap 的 mappings 字段是一个字符串,它描述了源文件和生成文件之间的映射关系。这个字符串被分成多个部分,每个部分对应源文件的一行。每个部分由一系列的映射组成,每个映射描述了源文件中的一个字符在生成文件中的位置。

每个映射由五个部分组成:

  1. 生成文件中的列号。
  2. 源文件中的行号。
  3. 源文件中的列号。
  4. 源文件中的名称索引。
  5. 源文件中的名称。

每个部分都使用 VLQ(Variable-length quantity)编码,这是一种压缩数字的方法。VLQ 编码使用一个或多个字节来表示一个数字,每个字节的最高位用于指示是否还有更多的字节需要读取。

下面是一个 mappings 字段的示例:

AAAA,SAASA,EAAM,CAAE,GAAG,EAAE,CACC,CAAC

在这个示例中,AAAA 表示生成文件中的第一列对应源文件的第一行第一列,SAASA 表示生成文件中的第二列对应源文件的第二行第二列,以此类推。

source-map库获取源文件信息

Node.js的source-map库可以做map文件的解析:

const sourcemap = require("source-map");

const mapFile = fs.readFileSync("./extension.js.map");
const rawSourceMap = JSON.parse(mapFile.toString());

const nameMap = new Map();
const fileMap = new Map();

await sourcemap.SourceMapConsumer.with(rawSourceMap, null, (consumer) => {
  consumer.eachMapping(function (m) {
    if (m.name) {
      nameMap.set(`${m.generatedLine}:${m.generatedColumn}`, m);
    }

    if (m.source) {
      if (!fileMap.has(m.source)) {
        fileMap.set(m.source, {
          start: m,
        });
      } else {
        fileMap.set(m.source, {
          ...fileMap.get(m.source),
          end: m,
        });
      }
    }
  });
});

上面我们使用source-map工具解析了变量名的映射关系以及对应的源文件路径信息,将解析的结果存在了两个map当中,便于我们后面进行读取。

AST处理节点命名

function updateNodeName(node, name) {
  if (node.type === "VariableDeclarator") {
    node.id.name = name;
  } else if (node.type === "Identifier") {
    node.name = name;
  } else if (node.type === "CallExpression") {
    updateNodeName(node.callee, name);
  } else if (node.type === "ArrowFunctionExpression") {
    node.params[0].name = name;
  } else if (node.type === "ExpressionStatement") {
    updateNodeName(node.expression, name);
  } else if (node.type === "AssignmentExpression") {
    updateNodeName(node.left, name);
  } else if (node.type === "MemberExpression") {
    updateNodeName(node.object, name);
  } else if (node.type === "BinaryExpression") {
    updateNodeName(node.left, name);
  } else if (node.type === "ConditionalExpression") {
    updateNodeName(node.test, name);
  } else if (node.type === "LogicalExpression") {
    updateNodeName(node.left, name);
  } else if (node.type === "SequenceExpression") {
    updateNodeName(node.expressions[0], name);
  } else if (node.type === "UpdateExpression") {
    updateNodeName(node.argument, name);
  } else if (node.type === "StringLiteral") {
    node.value = name;
  } else if (node.type === "AssignmentPattern") {
    updateNodeName(node.left, name);
  } else if (node.type === "ReturnStatement") {
    updateNodeName(node.argument, name);
  } else if (node.type === "BlockStatement") {
    updateNodeName(node.body[0], name);
  } else if (node.type === "UnaryExpression") {
    updateNodeName(node.argument, name);
  } else if (node.type === "ObjectProperty") {
    updateNodeName(node.key, name);
  } else if (node.type === "EmptyStatement") {
    // 好像是start和end标记
    // console.log(name);
  } else if (node.type === "NumericLiteral") {
    node.value = name;
  } else if (node.type === "ThisExpression") {
    // console.log(name);
  } else if (node.type === "ForStatement") {
    updateNodeName(node.init, name);
  } else if (node.type === "OptionalMemberExpression") {
    updateNodeName(node.object, name);
  } else if (node.type === "OptionalCallExpression") {
    updateNodeName(node.callee, name);
  } else if (node.type === "LabeledStatement") {
    updateNodeName(node.label, name);
  } else if (node.type === "ClassPrivateProperty") {
    updateNodeName(node.key, name);
  } else if (node.type === "PrivateName") {
    updateNodeName(node.id, name);
  } else if (node.type === "ClassPrivateMethod") {
    updateNodeName(node.key, name);
  } else if (node.type === "VariableDeclaration") {
    updateNodeName(node.declarations[0], name);
  } else if (node.type === "IfStatement") {
    updateNodeName(node.test, name);
  } else if (node.type === "TryStatement") {
    updateNodeName(node.block, name);
  } else if (node.type === "SwitchStatement") {
    updateNodeName(node.discriminant, name);
  } else {
    // console.log(node);
  }
}

我们封装了一个updateNodeName的方法,用于递归处理AST节点将变量命名给替换(这着实是一个体力活。。。),判断了各类表达式、语句、字面量等等场景。

AST映射源文件路径

对于文件路径我们怎样映射过去呢?目前已知的信息主要有:

  • 对应源代码的location
  • 对应源代码的path
  • 所有具有映射关系的map

一个比较自然的想法是基于代码的位置做字符切割。但是我们上面变量命名替换后,生成的新的代码文件行号和列号都已经发生了变化,无法映射到原来的行列,这条路很难行的通。

所以还是在AST遍历里面处理完比较好。

我们使用两个变量分别记录上一次遍历的文件路径和astnodes

let lastFile;
let lastNodes = [];

然后对AST进行遍历,先处理变量命名:

traverse(ast, {
    enter(p) {
      const node = p.node;
      const { line, column } = node.loc.start;
      
      // 处理变量命名
      if (nameMap.has(`${line}:${column}`)) {
        const sourceObj = nameMap.get(`${line}:${column}`);
        const name = sourceObj.name;
        updateNodeName(node, name);
      }
    },
  });

再处理路径:

traverse(ast, {
    enter(p) {
      const node = p.node;
      const { line, column } = node.loc.start;

      // 处理路径
      for (const [file, m] of fileMap.entries()) {
        if (
          m.start.generatedLine === line &&
          m.start.generatedColumn === column
        ) {
          if (lastFile && lastNodes.length) {
            const pp = path.resolve(__dirname, "./prettier/empty", lastFile);
            fs.ensureFileSync(pp);
            fs.writeFileSync(pp, lastNodes.map(n => generate(n).code).join());
            lastNodes = [];
          }
          lastFile = file;
          break;
        }
      }
      if (lastFile) {
        lastNodes.push(node);
        p.skip();
      }
    },
  });

注意这里的思路是每当start起始对应的文件路径发生变化的时候,生成上一个文件的源码,然后写入到对应的目录文件内。

另外ast是进行数组拼接的,我们需要通过skip方法防止children元素再次被递归到造成代码重复。

copilot的源码结构

最终得到的还原代码如下:

├── extension
   └── src
       ├── auth.ts
       ├── codeReferencing
          ├── codeReferenceEngagementTracker.ts
          ├── compute.ts
          ├── connectionState.ts
          ├── constants.ts
          ├── handleCopliotToken.ts
          ├── handlePostInsertion.ts
          ├── headerContributor.ts
          ├── index.ts
          ├── logger.ts
          ├── matchNotifier.ts
          ├── outputChannel.ts
          ├── snippy
             ├── errorCreator.ts
             ├── index.ts
             ├── network.ts
             └── snippy.proto.ts
          └── telemetry
              └── handlers.ts
       ├── config.ts
       ├── constants.ts
       ├── copilotPanel
          ├── common.ts
          ├── copilotListDocument.ts
          └── panel.ts
       ├── diagnostics.ts
       ├── experiments
          └── expFilters.ts
       ├── extension.ts
       ├── extensionStatus.ts
       ├── extensionTestApi.ts
       ├── fileSystem.ts
       ├── ghostText
          └── ghostText.ts
       ├── git.ts
       ├── install
          └── installationManager.ts
       ├── networkConfiguration.ts
       ├── proxy.ts
       ├── session.ts
       ├── statusBar.ts
       ├── statusBarPicker.ts
       ├── suggestions.ts
       ├── symbolDefinitionProvider.ts
       ├── telemetry.ts
       ├── telemetryDelegation.ts
       ├── textDocument.ts
       ├── textDocumentManager.ts
       └── vscodeCommitFileResolver.ts
├── lib
   └── src
       ├── auth
          ├── copilotToken.ts
          ├── copilotTokenManager.ts
          ├── copilotTokenNotifier.ts
          └── error.ts
       ├── changeTracker.ts
       ├── clock.ts
       ├── commitFileResolver.ts
       ├── common
          ├── cache.ts
          ├── debounce.ts
          ├── iterableHelpers.ts
          └── productContext.ts
       ├── config.ts
       ├── constants.ts
       ├── context.ts
       ├── copilotPanel
          ├── common.ts
          └── panel.ts
       ├── cursorHistoryManager.ts
       ├── defaultHandlers.ts
       ├── diagnostics.ts
       ├── documentTracker.ts
       ├── error
          └── userErrorNotifier.ts
       ├── experiments
          ├── defaultExpFilters.ts
          ├── expConfig.ts
          ├── features.ts
          ├── fetchExperiments.ts
          ├── filters.ts
          ├── granularityDirectory.ts
          └── granularityImplementation.ts
       ├── ghostText
          ├── completionsCache.ts
          ├── contextualFilter.ts
          ├── contextualFilterConstants.ts
          ├── contextualFilterTree.ts
          ├── copilotCompletion.ts
          ├── debounce.ts
          ├── ghostText.ts
          ├── multilineModel.ts
          ├── multilineModelWeights.ts
          ├── normalizeIndent.ts
          └── telemetry.ts
       ├── headerContributors.ts
       ├── installationManager.ts
       ├── language
          ├── generatedLanguages.ts
          ├── languageDetection.ts
          └── languages.ts
       ├── logger.ts
       ├── network
          ├── certificateReaderCache.ts
          ├── certificateReaders.ts
          ├── certificates.ts
          ├── helix.ts
          ├── proxy.ts
          └── proxySockets.ts
       ├── networkConfiguration.ts
       ├── networking.ts
       ├── notificationSender.ts
       ├── openai
          ├── config.ts
          ├── fetch.fake.ts
          ├── fetch.ts
          ├── openai.ts
          └── stream.ts
       ├── postInsertion.ts
       ├── postInsertionNotifier.ts
       ├── progress.ts
       ├── prompt
          ├── neighborFiles
             ├── cocommittedFiles.ts
             ├── cursorHistoryFiles.ts
             ├── neighborFiles.ts
             ├── openTabFiles.ts
             └── workspaceFiles.ts
          ├── parseBlock.ts
          ├── prompt.ts
          ├── promptLibProxy.ts
          ├── repository.ts
          ├── retrieval.ts
          └── symbolDefinition.ts
       ├── repositoryControl
          ├── constants.ts
          ├── contentRestrictions.ts
          ├── policyEvaluator.ts
          ├── repositoryControl.ts
          └── repositoryControlManager.ts
       ├── suggestions
          ├── anomalyDetection.ts
          ├── editDistance.ts
          ├── mlConstants.ts
          ├── restraint.ts
          └── suggestions.ts
       ├── telemetry
          ├── auth.ts
          ├── azureInsights.ts
          ├── azureInsightsReporter.ts
          ├── failbot.ts
          └── userConfig.ts
       ├── telemetry.ts
       ├── testing
          ├── config.ts
          ├── copilotToken.ts
          ├── packageRoot.ts
          ├── runtimeMode.ts
          ├── telemetry.ts
          ├── telemetryFake.ts
          ├── testHelpers.ts
          └── tokenManager.ts
       ├── textDocument.ts
       ├── textDocumentManager.ts
       ├── util
          ├── documentEvaluation.ts
          ├── nodeVersion.ts
          ├── opener.ts
          ├── redaction.ts
          ├── shortCircuit.ts
          └── typebox.ts
       └── workspaceFileSystem.ts
├── package.json
└── prompt
    └── src
        ├── elidableText
           └── index.ts
        └── tokenization
            └── index.ts

可以看到,copilot源码大体结构上就是extensionlib两层,extension是整个插件的入口,lib是底层封装的基础能力,在这个清晰的源码结构上,有助于我们进一步分析理解其中的逻辑。

上述代码已经提交在Github上,有需要的小伙伴可自取:

https://github.com/mengjian-github/copilot-analysis-new

小结一下

其实目前的copilot版本webpack混淆压缩要比之前更加难以分析,整个模块信息基本上无法拆离出来了,甚至是他们之间的依赖关系也变得更加模糊。不过copilot团队暴露了sourcemap文件,又可以进一步探求源码的结构和替换一些关键变量,为分析带来了一定的便利。

本文没有像之前那样将主流程再分析一遍,实际上,虽然代码组织结构清晰了,但是源码依旧无法完美还原,sourcemap其实帮助也有限,很多映射还需要不断对比生成代码进行推敲分析,基于这个版本的基础,要想深入了解,还是需要大量的时间和精力推导内在的逻辑实现。

copilot-analysis-new's People

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.