惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
S
Secure Thoughts
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
T
Tenable Blog
Know Your Adversary
Know Your Adversary
Webroot Blog
Webroot Blog
V
Vulnerabilities – Threatpost
WordPress大学
WordPress大学
S
Security @ Cisco Blogs
J
Java Code Geeks
S
SegmentFault 最新的问题
A
Arctic Wolf
Simon Willison's Weblog
Simon Willison's Weblog
T
Threatpost
Forbes - Security
Forbes - Security
爱范儿
爱范儿
T
The Blog of Author Tim Ferriss
S
Securelist
MongoDB | Blog
MongoDB | Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
月光博客
月光博客
Blog — PlanetScale
Blog — PlanetScale
博客园 - 【当耐特】
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
SecWiki News
SecWiki News
aimingoo的专栏
aimingoo的专栏
腾讯CDC
U
Unit 42
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
I
InfoQ
V2EX - 技术
V2EX - 技术
The Cloudflare Blog
V
V2EX
The Register - Security
The Register - Security
博客园 - Franky
Security Archives - TechRepublic
Security Archives - TechRepublic
Security Latest
Security Latest
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
云风的 BLOG
云风的 BLOG
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
小众软件
小众软件
N
Netflix TechBlog - Medium
量子位
M
MIT News - Artificial intelligence

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog NAT 类型初探 | Yesterday17's Blog
从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog
Yesterday17 · 2020-12-20 · via Yesterday17's Blog

CUE 这个格式对我而言一直是个很神秘的存在。在各种地方都能看见它的身影,直接打开也能看懂一些东西,但想要细说就做不到了。说到底,还是不知道它到底代表了什么,只能把它当播放列表来看。于是下定了决心要好好研究一下,于是就有了这篇文章。

ToC

  • 研究目标
  • 开始
  • 空白符与非空白符
  • 词法分析器选项
  • 开始条件
  • 字符串
    • 显式字符串
    • 隐式字符串
    • 字符串处理
  • 词法单元
    • 依赖后续名称(1)
    • 简单词法单元
    • 依赖后续名称(2)
  • 注释(REM
    • 注释类型
    • 空白字符与错误容忍
    • 结束
  • 回放增益(RPG
    • 回访增益处理
    • 空字符处理
    • SKIP 处理
  • 全局空字符处理
  • 数字
  • 冒号
  • 空行
  • 结束:全局错误
  • 结语
  • 参考

研究目标

我们的研究目标是 libcue[1]。根据仓库的说法,这是 cuetoolscue 相关部分的 fork 增强。不管怎么说,这是一个 CUE 解释器的完整实现。

这篇文章我们来研究其中的词法分析部分[2]。

开始

%{

/*

* Copyright (c) 2004, 2005, 2006, 2007, Svend Sorensen

* Copyright (c) 2009, 2010 Jochen Keil

* For license terms, see the file COPYING in this distribution.

*/

#include <stdlib.h>

#include <string.h>

#include "cd.h"

#include "cue_parser.h"

char yy_buffer[PARSER_BUFFER];

int yylex(void);

%}

开头没什么太多特别的。根据 lex 的语法,由 %{}% 包裹的部分都会被原样复制到输出中。

空白符与非空白符

ws [ \t\r]

nonws [^ \t\r\n]

这里定义了空格、制表符和 \r空白符 ws,而除空白符和 \n 之外的符号都为非空白符 nonws

词法分析器选项

%option yylineno

%option noyywrap

%option noinput

%option nounput

接下来的四行规定了词法分析器的一些选项,具体如下:

  • yylineno:使用全局变量 yylineno 表示当前词法分析的行号
  • noyywrap:不定义 yywrap 函数
  • noinput:不使用 input 函数
  • nounput:不适用 unput 函数

关于 noinputnounput 的相关信息,可以参考 StackOverflow 的这篇文章[3]。

开始条件

%s NAME

%x REM

%x RPG

%x SKIP

%s%x 可以用于定义词法规则的开始条件。其中:

  • %s 定义包含性(inclusive)的开始条件,匹配时包含无开始条件的规则。
  • %x 定义排他性(exclusive)的开始条件,匹配时不包含无开始条件的规则。

通过中括号 <nameA, nameB> 使用开始条件,逗号表示关系或

开始条件 INITIAL 表示默认的开始条件,即无开始条件。

通过 BEGIN 行为(Action)激活对应的开始条件,如:BEGIN(INITIAL)

更加详细的说明可以查阅文档[4]。

字符串

显式字符串

\'([^\']|\\\')*\' |

\"([^\"]|\\\")*\" {

yylval.sval = strncpy( yy_buffer,

++yytext,

(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng));

yylval.sval[(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng) - 2] = '\0';

BEGIN(INITIAL);

return STRING;

}

显式字符串表示的是由引号(单引号或双引号)包裹的字符串,通过转义字符 \ 可以保留引号本身。

隐式字符串

<NAME>{nonws}+ {

yylval.sval = strncpy( yy_buffer,

yytext,

(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng));

yylval.sval[(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)] = '\0';

BEGIN(INITIAL);

return STRING;

}

隐式字符串表示的是不由引号包裹的字符串。这样的字符串在开始条件 NAME 之后出现,由非空白字符nonws)组成。

典型的此类字符串是 REM GENRE Game。在这个字符串中,REMGENREToken,而 Game 则是一个没有引号包裹的字符串。

字符串处理

字符串捕获后被复制到 yy_buffer 中,返回 Token 的类型为 STRING

字符串捕获完成后,开始条件被重设为 INITIAL

词法单元

依赖后续名称(1)

CATALOG { BEGIN(NAME); return CATALOG; }

CDTEXTFILE { BEGIN(NAME); return CDTEXTFILE; }

FILE { BEGIN(NAME); return FFILE; }

上述的三个 Token,包括 CATALOGCDTEXTFILEFILE 都依赖后续字符串。

简单词法单元

BINARY { return BINARY; }

MOTOROLA { return MOTOROLA; }

AIFF { return AIFF; }

WAVE { return WAVE; }

MP3 { return MP3; }

FLAC { return FLAC; }

TRACK { return TRACK; }

AUDIO { yylval.ival = MODE_AUDIO; return AUDIO; }

MODE1\/2048 { yylval.ival = MODE_MODE1; return MODE1_2048; }

MODE1\/2352 { yylval.ival = MODE_MODE1_RAW; return MODE1_2352; }

MODE2\/2336 { yylval.ival = MODE_MODE2; return MODE2_2336; }

MODE2\/2048 { yylval.ival = MODE_MODE2_FORM1; return MODE2_2048; }

MODE2\/2342 { yylval.ival = MODE_MODE2_FORM2; return MODE2_2342; }

MODE2\/2332 { yylval.ival = MODE_MODE2_FORM_MIX; return MODE2_2332; }

MODE2\/2352 { yylval.ival = MODE_MODE2_RAW; return MODE2_2352; }

FLAGS { return FLAGS; }

PRE { yylval.ival = FLAG_PRE_EMPHASIS; return PRE; }

DCP { yylval.ival = FLAG_COPY_PERMITTED; return DCP; }

4CH { yylval.ival = FLAG_FOUR_CHANNEL; return FOUR_CH; }

SCMS { yylval.ival = FLAG_SCMS; return SCMS; }

PREGAP { return PREGAP; }

INDEX { return INDEX; }

POSTGAP { return POSTGAP; }

上述的这些都是简单词法单元,只需要进行文本匹配即可。

依赖后续名称(2)

TITLE { BEGIN(NAME); yylval.ival = PTI_TITLE; return TITLE; }

PERFORMER { BEGIN(NAME); yylval.ival = PTI_PERFORMER; return PERFORMER; }

SONGWRITER { BEGIN(NAME); yylval.ival = PTI_SONGWRITER; return SONGWRITER; }

COMPOSER { BEGIN(NAME); yylval.ival = PTI_COMPOSER; return COMPOSER; }

ARRANGER { BEGIN(NAME); yylval.ival = PTI_ARRANGER; return ARRANGER; }

MESSAGE { BEGIN(NAME); yylval.ival = PTI_MESSAGE; return MESSAGE; }

DISC_ID { BEGIN(NAME); yylval.ival = PTI_DISC_ID; return DISC_ID; }

GENRE { BEGIN(NAME); yylval.ival = PTI_GENRE; return GENRE; }

TOC_INFO1 { BEGIN(NAME); yylval.ival = PTI_TOC_INFO1; return TOC_INFO1; }

TOC_INFO2 { BEGIN(NAME); yylval.ival = PTI_TOC_INFO2; return TOC_INFO2; }

UPC_EAN { BEGIN(NAME); yylval.ival = PTI_UPC_ISRC; return UPC_EAN; }

ISRC/{ws}+\" { BEGIN(NAME); yylval.ival = PTI_UPC_ISRC; return ISRC; }

SIZE_INFO { BEGIN(NAME); yylval.ival = PTI_SIZE_INFO; return SIZE_INFO; }

ISRC { BEGIN(NAME); return TRACK_ISRC; }

这些词法单元在依赖后续名称(NAME)的基础上还调整了对应的 yylval 的值。

注释(REM

注释类型

<REM>DATE { BEGIN(NAME); yylval.ival = REM_DATE; return DATE; }

<REM>GENRE { BEGIN(NAME); yylval.ival = PTI_GENRE; return XXX_GENRE; }

<REM>REPLAYGAIN_ALBUM_GAIN { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_ALBUM_GAIN;

return REPLAYGAIN_ALBUM_GAIN; }

<REM>REPLAYGAIN_ALBUM_PEAK { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_ALBUM_PEAK;

return REPLAYGAIN_ALBUM_PEAK; }

<REM>REPLAYGAIN_TRACK_GAIN { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_TRACK_GAIN;

return REPLAYGAIN_TRACK_GAIN; }

<REM>REPLAYGAIN_TRACK_PEAK { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_TRACK_PEAK;

return REPLAYGAIN_TRACK_PEAK; }

这里定义了 REM 的种类,包括 DATEGENRE 等六种类型。其中 REPLAYGAIN 的四种在解析后会进入 RPG 开始条件。

空白字符与错误容忍

<REM>{ws}+ { BEGIN(REM); }

<REM>. { BEGIN(REM); }

REM 条件下跳过所有的空白字符,而对于没有被之前规则匹配到的任意字符(.)也跳过处理。

结束

<REM>\n { BEGIN(INITIAL); }

当遇到换行符时,REM 结束。这也意味着 REM 注释只能在一行内完成。

回放增益(RPG

回放增益(英语:Replay Gain)是一个于2001年7月12日被公开提出的标准,用于将像 MP3Ogg Vorbis 等格式的数字化音频的可感知响度进行标准化(Normalize)处理[5]。

回访增益处理

<RPG>{nonws}+ {

yylval.sval = strncpy( yy_buffer,

yytext,

(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng));

yylval.sval[(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)] = '\0';

BEGIN(SKIP);

return STRING;

}

回访增益只取一个由非空字符构成的串。在获得这个字符串后,进入 SKIP 开始条件。

空字符处理

<RPG>{ws}+ { BEGIN(RPG); }

RPG 中忽视所有空字符。

SKIP 处理

<SKIP>.*\n { BEGIN(INITIAL); yylineno++; return '\n'; }

忽略所有 SKIP 条件下的字符,直到新行出现,进入 INITIAL 状态。

全局空字符处理

{ws}+ { /* ignore whitespace */ }

全局忽略空字符。

数字

[[:digit:]]+ { yylval.ival = atoi(yytext); return NUMBER; }

对数字进行解析。

冒号

不是很清楚为什么冒号需要单独处理,总之这里是捕获了冒号(

空行

^{ws}*\n { yylineno++; /* blank line */ }

\n { yylineno++; return '\n'; }

定义只存在空白字符或不存在字符的行为空行,对空行只增加行号,不进行其他处理。

结束:全局错误

. { fprintf(stderr, "bad character '%c'\n", yytext[0]); }

对解析到这一步还没有被辨识的字符,报 bad character 错误。

结语

对于 CUE 格式,还有其他各种资料可考。在这篇文章写完之后,我在 GitHub 上找到了一份整理好的说明[6],可供参考。

这里遗漏了一些很关键的东西,比如词法单元的声明,这些我们留到下一篇介绍 cue_parser.y 的时候再谈也不迟(笑)

参考

  1. https://github.com/lipnitsk/libcue
  2. https://github.com/lipnitsk/libcue/blob/f6a11cbfd6029abb9cbc50264d2b747d3f9e427f/cue_scanner.l
  3. https://stackoverflow.com/questions/39075510/option-noinput-nounput-what-are-they-for
  4. http://dinosaur.compilertools.net/flex/flex_11.html
  5. https://zh.wikipedia.org/wiki/%E5%9B%9E%E6%94%BE%E5%A2%9E%E7%9B%8A
  6. https://github.com/libyal/libodraw/blob/main/documentation/CUE%20sheet%20format.asciidoc