3
3
2012
6

GM 脚本:github 默认下载格式改为 gzip

不知道为什么,新版 github 在项目首页上只提供 zip 归档格式的下载,要下载 gzip 包还需要点击 download 到新页面去。可是我就是不喜欢 zip 格式嘛——

// ==UserScript==
// @name           github 下载默认 gzip 格式
// @namespace      http://lilydjwg.is-programmer.com/ 
// @description    把默认的 zip 下载按键改成 gzip 的
// @include        https://github.com/*
// ==/UserScript==
 
var dl = document.querySelector('.btn-download');
if(dl){
  dl.title = dl.title.replace('zip', 'gzip')
  dl.href = dl.href.replace('/zipball/', '/tarball/');
  var icon = dl.querySelector('.icon');
  if(icon){
    icon.nextSibling.textContent = 'GZIP';
  }
}
Category: 火狐 | Tags: GreaseMonkey 火狐 linux github
3
2
2012
6

为 Chito 修改 Markdown

我使用 Markdown 写博客已经有段时间了,但是一直以来有个小小的问题:对于代码块,markdown 生成的是一个<pre>标签里套一个<code>标签。缩进四个空格还好,用 Vim 的列编辑就行了(>操作不行,因为空行不会被缩进),可是删除这些<code>标签并加上相应的语言标识很烦。于是有了以下 Python 代码,使用的是 Python 版的 markdown,支持使用~~~~作为代码分隔符,如:

~~~~python|这是 Python 代码
print('Hello Python!')
~~~~

将会被翻译为

<pre class="brush: python;" title="这是 Python 代码">print('Hello Python!')
</pre>

程序如下:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import sys
from itertools import takewhile

import markdown
from lxml.html import fromstring, tostring

def parseAttr(s):
  a = s.split('|')
  if len(a) > 3:
    raise ValueError('Too many attributes')
  a = list(map(str.strip, a))
  if len(a) == 3:
    a[2] = bool(a[2])
  elif len(a) == 2:
    a.append(False)
  elif len(a) == 1:
    a.extend(['', False])
  else:
    a = ['plain', '', False]
  if not a[0]:
    a[0] = 'plain'
  return a

def analyseAttrs(text):
  '''Attributes are defined like this:

  ~~~~lang|title|collapse

  In place of ``collapse``, anything not empty is considered true.
  '''
  incode = False
  lines = []
  attrs = []
  istilda = lambda ch: ch == '~'
  for l in text.split('\n'):
    if l.startswith('~~~~'):
      if not incode:
        incode = len(tuple(takewhile(istilda, l)))
        attr = parseAttr(l.lstrip('~'))
        attrs.append(attr)
        l = tildas = '~' * incode
      else:
        if l.find(tildas) == 0:
          incode = False
    lines.append(l)
  return '\n'.join(lines), attrs

def applyAttrs(html, attrs):
  doc = fromstring(html)
  for i, code in enumerate(doc.xpath('//pre/code')):
    pre = code.getparent()
    text = pre[0].text
    del pre[:]
    pre.text = text
    attr = attrs[i]
    c = 'brush: %s;' % attr[0]
    if attr[2]:
      c += ' collapse: true;'
    pre.set('class', c)
    if attr[1]:
      pre.set('title', attr[1])
  return tostring(doc, encoding=str)[5:-6] + '\n'

def main():
  text = sys.stdin.read()
  text, attrs = analyseAttrs(text)
  out = markdown.markdown(text, ['fenced_code'])
  out = applyAttrs(out, attrs)
  sys.stdout.write(out)

if __name__ == '__main__':
  main()
Category: python | Tags: chito markdown python
3
2
2012
6

在 fcitx 中切换国标与传统引号

国家标准使用这些引号:‘’“”,而我发现传统中文的引号更漂亮:「」『』。我切换到传统引号已经有一段时间了,但最近发现有时还是需要使用国标引号,而 fcitx 的现任开发者认为不需要加入该切换功能。好在 fcitx 的配置文件都是文本,又有 fcitx-remote 工具,所以自己很容易地手动实现了两个版本的——Haskell 版本纯粹是练习用,因为没有扩展路径中的~的现成函数,所以只好自己找了个实现,代码有些长。

import Control.Applicative ((<$>))
import System.Cmd (rawSystem)
import System.Directory (getHomeDirectory)
import System.Posix.User
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

main = do
  file <- getFile
  TIO.readFile file >>= (TIO.writeFile file) . (T.map (trChar "“”‘’『』「」" "『』「」“”‘’"))
  reloadFcitx

getFile :: IO FilePath
getFile = expandUser "~/.config/fcitx/data/punc.mb.zh_CN"

reloadFcitx :: IO ()
reloadFcitx = rawSystem "fcitx-remote" ["fcitx-remote", "-r"] >> return ()

expandUser :: FilePath -> IO FilePath
expandUser "~"         = getHomeDirectory
expandUser ('~':'/':p) = fmap (++ "/" ++ p) getHomeDirectory
expandUser ('~':up)    = let (u, p) = break (== '/') up
                             in fmap (++ tail p) (homeDirectory <$> getUserEntryForName u)
expandUser p           = return p

trChar :: [Char] -> [Char] -> Char -> Char
trChar from to ch = case i of
                         Just i -> to !! i
                         _      -> ch
                         where i = elemIndex ch from

Python 版本就很简洁了:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

import os

m = str.maketrans('“”‘’『』「」', '『』「」“”‘’')
file = os.path.expanduser("~/.config/fcitx/data/punc.mb.zh_CN")

c = open(file).read().translate(m)
open(file, 'w').write(c)
os.execvp('fcitx-remote', ['fcitx-remote', '-r'])
Category: Linux | Tags: fcitx Haskell python
2
28
2012
8

拯救分区表

今天,因为弄错了磁盘顺序,不小心把移动硬盘的前 40M 左右的数据覆盖掉了。所幸发现的时候移动硬盘还接在电脑上,除了第一个分区外,后边的还可以正常挂载和读取,这说明内核还记得分区表。但是fdisk已经读不到 MBR 了。

怎么办呢?我不想把几百 G 的数据拷一遍,而且理论上肯定是能够把分区表给完整的找回来的——内核不是还记得么?上网只搜到/proc/partitions这个文件,其中的内容如下:

major minor  #blocks  name

   8        0  312571224 sda
   8        1   52436128 sda1
   8        2   52428800 sda2
   8        3          1 sda3
   8        5     512000 sda5
   8        6   41943040 sda6
   8        7  161281024 sda7
   8        8    3964928 sda8
  11        0    1048575 sr0
   7        0      71680 loop0
   8       16  312571224 sdb
   8       17    1048576 sdb1
   8       18  209715200 sdb2
   8       19   10485760 sdb3
   8       20   91320320 sdb4

只有分区的大小信息,而且单位是。我按移动硬盘的大小推算了下,这里的块大小是1KiB(关于块大小,真够混乱的。ls默认的也是 1K,但是dd却是 512B)。

光知道了块大小不行啊。我先试了试著名的 testdisk 工具。它搜索了好久,最终只找到了两个分区,于是被我否决了。又继续找分区的更多信息。/proc下看完了,我又去不怎么了解的/sys下看,发现其下有个block目录,里面正是系统已经识别的块设备!

进到出事故的sdb下,再进入sdb1ls一下,看到sizestart都在呢!cat出来各是一个整数。经过一番猜测和计算,可以确定其单位是512B,也就是一个扇区

好了,可以开始重建分区表了。当然,我可不想手工去算和写那64字节的二进制数据。试了试 Arch 安装时所用的cfdisk。它有基于文本的图形化界面,比较友好。可是新建分区时才发现只能输入以1000进制MB为单位的大小,而我需要分毫不差的按原大小分区。只好退出,试试文本交互的fdisk。在不断地按m键查看帮助的情况下,终于把分区重建好了:

  Device Boot      Start         End      Blocks   Id  System
/dev/sdb1            2048     2099199     1048576   83  Linux
/dev/sdb2         2099200   421529599   209715200   83  Linux
/dev/sdb3       421529600   442501119    10485760   83  Linux
/dev/sdb4       442501120   625141760    91320320+   7  HPFS/NTFS/exFAT

前面几个的大小比较整,我是按+1G等这样输入的。这里要注意下的是,G, M, K等单位是1024进制,而GB, MB, KB等单位是1000进制。最后那个给 Windows 留的 NTFS 分区不知道为什么并不是在磁盘的最后一个扇区结束的,我输入的是/sys/block/sdb/sdb4/size里写的大小。

p查看并确认分区表正确后,按w写入。然后使用partprobe命令通知内核更新分区表信息。

这步做完后,后边的三个分区就安全了。第一个分区是一些启动文件,我已经打算重新弄一遍了。实际上fsck.ext2跑完后也只是在lost+found里出现了一堆垃圾文件。mkfs.ext2重新格式化,却在安装 grub2 时遇到了问题。大致的错误信息是这样的:

/dev/sdb appears to contain a iso9660 filesystem which isn't known to reserve space for DOS-style boot.  Installing GRUB there could result in FILESYSTEM DESTRUCTION if valuable data is overwritten by grub-setup (--skip-fs-probe disables this check, use at your own risk)

中文消息是:

/dev/sda 中似乎包含一个不为 DOS 引导保留空间的 iso9660 文件系统。在此处安装 GRUB 可能导致 grub-setup 覆盖重要数据从而损坏文件系统(--skip-fs-probe 参数可以禁用这个检查,使用该选项风险自负)

这个「iso9660」文件系统就是我误dd过去的。使用grub-setup并加上--skip-fs-probe参数后依旧出错:

warn: Attempting to install GRUB to a disk with multiple partition labels or both partition label and filesystem.
error: embedding is not possible, but this is required for cross-disk install.

中文消息是:

警告:正在试图将 GRUB 安装至有多个分区标签的磁盘,或同时有分区标签和文件系统的磁盘。这样的操作尚未被支持。
错误:无法嵌入,但在跨盘安装时是必须的

加上--force参数也没有用。我尝试消除前446字节的数据,亦没有用。后来想起在使用fdisk分区时,第一分区的起始扇区必须大于等于2048。难道是这些扇区中的内容影响了 grub2 的安装?head -c 1024 > a然后用 bviplus 查看,发现果然如此,都看到那个已经不完整的 iso9660 文件系统的卷标了。果然给它 dd 掉:

sudo dd if=/dev/zero of=/dev/sdb seek=1 count=2047 bs=1b

再次尝试安装,一切顺利!

Category: Linux | Tags: linux 失误 数据恢复
2
23
2012
8

迁移到64位 Arch Linux

曾经因为不了解,所以一开始选择了32位系统;后来内存大了,ArchLinux 的内核没有编译PAE支持,只能用 3GiB 多的内存,不爽。朋友又给了这篇教程,于是略作准备就开始动手了。

可是,但我真正动手时,那篇文章已经被另一篇英文教程取代了。当时忘记了 MediaWiki 会记录所有的编辑历史,所以只好将就着看了。结果,重启时 udev 没跑起来,说找不到 librt.so。我当然不会就此罢休,花费了一些时间,不仅拯救了系统,成功迁移到了 64 位,而且确定了 ArchWiki 上那篇教程的错误。

先说这个错误吧。现在已经修正了。方法 2: 从正在运行的系统一节,在安装 busybox 的时候,现在是一个红色的警告框,暗示着这里曾经发生的悲剧——

警告: 不要现在安装 lib32-glibc 软件包。在执行命令 ldconfig 后,当你安装 linux(内核)时,生成的镜像文件中,librt.so 等库文件会在 /usr/lib32 目录下,启动的时候二进制文件不会在此搜索库文件,导致启动失败。

当悲剧发生后,我有些紧张地拿出移动硬盘,先是进入移动硬盘上的 Arch,胡乱安装了几个 64 位软件包并重新生成 initramfs,结果连 init 也执行不了了。无奈我又启动64位内核的 PartedMagic chroot 进去查看。但是,chroot 失败了:

sh: cannot open shared object file

后来我才知道,因为我安装lib32-glibc/lib/ld-linux.so.2发生冲突,我覆盖了;后来卸载 32 位的 glibc 时,它被删除了。于是,动态链接的 32 位程序没有动态库加载器了。但其实还是有办法的,因为被误删的只是个软链接。

/usr/lib32/ld-linux.so.2 /bin/ls

老猫的提示下,手动指定ld-linux.so.2运行成功。我尝试把它链接过去,这个 32 位与 64 位库混合的系统开始有些可用了。接下来按 Wiki 里的指示操作就可以了。不过,我没有重装全部的库,而是只装了标明 i686 架构的库。这就是我之前那个用 Haskell 写的脚本的目的。不过还是出了点小问题——虽然我为防止程序运行不了而在开始之前把软件包列表生成了一份保存起来,但我忘了我的 HOME 是加密过的。PartedMagic 里没有 eCryptfs 工具,而 Arch 里的那个又因为库的原因运行不了,囧死了。。。幸运的是,我在迁移之前把 Dropbox 弄好同步了一遍,这个软件包列表也同步了。通过 Dropbox 网页界面下回来就好了。

弄好重启之后,整个事情还没完——我从 AUR 里编译安装的那些包还没重装呢。边重装边研究新的 64 位系统,发现 gcc 还是可以编译出 32 位程序的,只是要装gcc-multilib而已。现在可好了,既可以运行 64 位的程序,又可以运行 32 位的。库依赖少的 C 程序编译个 32 位的出来也没问题。

对了,最后说一句,vnstat 这厮的数据库格式竟然是平台相关的!换成 64 位后它就不断报错,直到我删除了以前的数据库。

Category: Linux | Tags: arch linux
2
17
2012
5

Haskell 实战:使用 Parsec 解析 lrc 歌词文件

既然来学 Haskell 了,Parsec 不应该错过。lrc 文件的格式大家应该都清楚。虽然说它用正则表达式解析很容易也很可靠,但是,我这不是练习么!

数据类型的定义

首先,我们想想歌词文件解析出来有些什么。主要数据当然是一条条带时间的歌词!除此之外,还会可选地有歌名啦歌手啦之类的东西。

先来定义一条歌词,也就是一个最高精确到百分之一秒的时间,和一个字符串。也就是:

data LrcLine = LrcLine {
  time :: Int,
  line :: String
} deriving (Eq, Show, Ord)

我们需要实现Ord类型类以便比较,因为 lrc 文件的歌词有一种紧凑的格式,在相同的歌词前有多个时间。这时,歌词就不是排好序的了。GHC 会自动推断出比较函数,也就是逐个域地进行比较。也可以手动定义其为Ord的实例:

-- import Data.Function (on)
instance Ord LrcLine where
  compare = compare `on` time

然后是整个歌词文件的信息:

data Lrc = Lrc {
  title :: Maybe String,
  artist :: Maybe String,
  album :: Maybe String,
  by :: Maybe String,
  metadata :: [(String, String)],
  lyrics :: [LrcLine]
}

因为可能会有未知的元信息,所以我们定义了一个metadata域来存储之。其类型为[(String, String)],以便使用lookup函数进行查询。

自顶向下设计解析器:顶层解析器

RWH的说明,似乎一般都不写解析器的类型签名。但既然是初学嘛,我还是写上好了——

lrcParser :: GenParser Char st Lrc

什么意思我还不太懂,不过最后那个Lrc很显然就是解析结果的类型啦。

我们的解析器先从歌词源文件中读取若干行的元信息,接下来读取所有的歌词数据,最后构造个 Lrc类型的数据。

lrcParser = do
  metadata <- many $ try lrcMeta
  ly <- concat <$> many lrcLine
  return Lrc {
    title = lookup "ti" metadata,
    artist = lookup "ar" metadata,
    album = lookup "al" metadata,
    by = lookup "by" metadata,
    metadata = metadata,
    lyrics = sort ly
  }

manytry都是 Parsec 里的函数。many接受一个类型为解析器的参数,在求值时它一直调用这个解析器,直到它不消耗输入为止。如果这个解析器消耗了输入却又没能成功,那么整个many解析器也就失败了。而try在消耗了任意数量的输入但没有最终成功时会把已消耗的输入退回去,结果是没有消耗输入。开个 GHCi 会话演示下:

>>> ghci
GHCi, version 7.0.3: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
ghci> import Text.ParserCombinators.Parsec
ghci> let p = string "ab" :: GenParser Char st String
Loading package transformers-0.2.2.0 ... linking ... done.
Loading package mtl-2.0.1.0 ... linking ... done.
Loading package bytestring-0.9.1.10 ... linking ... done.
Loading package array-0.3.0.2 ... linking ... done.
Loading package containers-0.4.0.0 ... linking ... done.
Loading package deepseq-1.1.0.2 ... linking ... done.
Loading package text-0.11.0.5 ... linking ... done.
Loading package parsec-3.1.2 ... linking ... done.
ghci> parse p "<string>" "abc"
Right "ab"
ghci> parse p "<string>" "ac"
Left "<string>" (line 1, column 1):
unexpected "c"
expecting "ab"
ghci> parse p "<string>" "d"
Left "<string>" (line 1, column 1):
unexpected "d"
expecting "ab"
ghci> parse p "<string>" ""
Left "<string>" (line 1, column 1):
unexpected end of input
expecting "ab"
ghci> parse (many p) "<string>" "ababc"
Right ["ab","ab"]
ghci> parse (many p) "<string>" "ababa"
Left "<string>" (line 1, column 5):
unexpected end of input
expecting "ab"
ghci> parse (many $ try p) "<string>" "ababa"
Right ["ab","ab"]

所以,many $ try lrcMeta就是不断尝试解析歌词元信息,直到解析失败时停止。

接下来是对歌词数据的解析。因为一行可能有多个时间,我们把它存储成多条LrcLine,所以需要使用concat来连接下每次调用lrcLine返回的结果列表。

自顶向下设计解析器:余下的部分

lrcMeta很简单,一行文本,由中括号括起来,其中的键和值用冒号隔开:

lrcMeta :: GenParser Char st (String, String)
lrcMeta = do
  char '['
  key <- many $ noneOf ":"
  char ':'
  val <- many $ noneOf "]"
  char ']'
  eol
  return (key, val)

lrcLine差不多,不过涉及到时间的解析:

lrcLine :: GenParser Char st [LrcLine]
lrcLine = do
  times <- many1 lrcTime
  line <- many $ noneOf "\r\n"
  optional eol
  return $ map (\t -> LrcLine {
    time = t,
    line = line
  }) times

嗯?没看到对时间的解析?哦,它在这里:

lrcTime :: GenParser Char st Int
lrcTime = do
  char '['
  minutes <- readInt
  char ':'
  second <- readInt
  centisec <- option 0 $ char '.' >> readInt
  char ']'
  return $ 60 * 100 * minutes + 100 * second + centisec
  where readInt = read <$> many digit

好了,你可以编译下试试了。RWH说过了,Compile early, compile often。这样在你不小心出错时,强大的编译器能够及时提示你。

哦,下边是 import 列表:

import Data.Char (isDigit)
import Data.Functor ((<$>))
import Data.List (sort)
import Data.Maybe (isJust, fromJust)
import Text.ParserCombinators.Parsec

你试过了吗?发生了什么?

是的,我还有个「抄袭」RWH的换行符解析器没列出来。链接在文末给出了,大家自己去找吧 ;-)

什么?你没找到?好吧,那你加上这个,也可以编译的了。其实类型的语句早该写的。

eol :: GenParser Char st String
eol = undefined

这样就定义了eol函数,它被定义为一个匹配任意类型的「未定义」值。

最后加点工具函数

一个给把 offset 加到歌词数据里的,另一个则是给歌词在时间轴上偏移一定时间的。

lrcAddOffset :: Lrc -> Lrc
lrcAddOffset l = l { lyrics = ly', metadata = meta' }
  where ly = lyrics l
    meta = metadata l
    offset = lookup "offset" meta >>= parseInt
    ly' = case offset of
          Just t -> addTime (fromInteger t `div` 10) ly
          otherwise -> ly
    meta' = filter notOffset meta
    notOffset = (/= "offset") . fst

addTime :: Int -> [LrcLine] -> [LrcLine]
addTime t = map $ \l -> l { time = (t + time l) }

嗯,还是个parseInt用来把字符串转成整数,并且很好地处理异常。

parseInt :: String -> Maybe Integer
parseInt s = case reads s of
  [(int, "")] -> Just int
  otherwise   -> Nothing

完整代码

-- module Text.Lrc (
--   parseLrc,
--   addTime,
--   lrcAddOffset,
--   Lrc(..),
-- ) where
-- 为测试,这个被注释掉了

import Data.Char (isDigit)
import Data.Functor ((<$>))
import Data.List (sort)
import Data.Maybe (isJust, fromJust)
import Text.ParserCombinators.Parsec

data Lrc = Lrc {
  title :: Maybe String,
  artist :: Maybe String,
  album :: Maybe String,
  by :: Maybe String,
  metadata :: [(String, String)],
  lyrics :: [LrcLine]
}

data LrcLine = LrcLine {
  time :: Int,
  line :: String
} deriving (Eq, Show, Ord)

lrcParser :: GenParser Char st Lrc
lrcParser = do
  metadata <- many $ try lrcMeta
  ly <- concat <$> many lrcLine
  return Lrc {
    title = lookup "ti" metadata,
    artist = lookup "ar" metadata,
    album = lookup "al" metadata,
    by = lookup "by" metadata,
    metadata = metadata,
    lyrics = sort ly
  }

lrcMeta :: GenParser Char st (String, String)
lrcMeta = do
  char '['
  key <- many $ noneOf ":"
  char ':'
  val <- many $ noneOf "]"
  char ']'
  eol
  return (key, val)

lrcLine :: GenParser Char st [LrcLine]
lrcLine = do
  times <- many1 lrcTime
  line <- many $ noneOf "\r\n"
  optional eol
  return $ map (\t -> LrcLine {
    time = t,
    line = line
  }) times

lrcTime :: GenParser Char st Int
lrcTime = do
  char '['
  minutes <- readInt
  char ':'
  second <- readInt
  centisec <- option 0 $ char '.' >> readInt
  char ']'
  return $ 60 * 100 * minutes + 100 * second + centisec
  where readInt = read <$> many digit

eol :: GenParser Char st String
eol = try (string "\n\r")
  <|> try (string "\r\n")
  <|> string "\n"
  <|> string "\r"
  <?> "end of line"

lrcAddOffset :: Lrc -> Lrc
lrcAddOffset l = l { lyrics = ly', metadata = meta' }
  where ly = lyrics l
        meta = metadata l
        offset = lookup "offset" meta >>= parseInt
        ly' = case offset of
                   Just t -> addTime (fromInteger t `div` 10) ly
                   otherwise -> ly
        meta' = filter notOffset meta
        notOffset = (/= "offset") . fst

addTime :: Int -> [LrcLine] -> [LrcLine]
addTime t = map $ \l -> l { time = (t + time l) }

parseInt :: String -> Maybe Integer
parseInt s = case reads s of
  [(int, "")] -> Just int
  otherwise   -> Nothing

main = getContents >>= \lrcfile -> case parse lrcParser "<stdin>" lrcfile of
  Left err -> print err >> error "Failed."
  Right lrc -> mapM_ print $ lyrics $ lrcAddOffset lrc

参考链接

Category: Haskell | Tags: Haskell
1
28
2012
5

使用 eCryptfs 加密主目录

本文根据回忆记述在 Arch Linux 上为某一新用户建立使用 eCryptfs 加密的 $HOME 目录并使之在登录时自动解密挂载的过程。大量参考了 Unknown and partly hidden 的 eCryptfs and $HOME 一文。

依赖的软件包:ecryptfs-utils。

加密目录

# mkdir -p /home/.ecryptfs/user/private
# chmod 755 /home/.ecryptfs
# chmod -R 700 /home/.ecryptfs/user
# chown -R user:user /home/.ecryptfs/user
# ln -s /home/.ecryptfs/user/private /home/user/.private
# chmod 700 /home/user

注意:最后一步原文使用的是500权限,这里改成了700

第一次挂载加密目录:

# mount -t ecryptfs /home/user/.private /home/user

eCryptfs 会询问一些加密的选项,其中 Cypher(加密方法)和 Key byte 可自行选择:

Key type: passphrase
Passphrase: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Cypher: twofish
Key byte: 32
Plaintext passtrough: yes
Filename encryption: yes
Add signature to cache: yes

一定要记住密码,虽然可能并不怎么会用到。

mount命令的输出中找到这次挂载使用的参数,经过一些变更,把类似于以下的设置添加到/etc/fstab中:

/home/user/.private /home/user ecryptfs rw,user,noauto,exec,ecryptfs_sig=XYZ,ecryptfs_cipher=twofish,ecryptfs_key_bytes=32,ecryptfs_passthrough,ecryptfs_fnek_sig=XYZ,ecryptfs_unlink_sigs 0 0

注意:在登录挂载时,noexecnosuidnodev将会是默认选项。这里加上exec选项来覆盖掉noexec,这样加密的 $HOME 中才支持执行可执行文件。

挂载时生成了/root/.ecryptfs目录。我们先在里边保存些文件:

# touch /root/.ecryptfs/auto-mount
# ecryptfs-wrap-passphrase /root/.ecryptfs/wrapped-passphrase
Passphrase to wrap: [输入加密口令]
Wrapping passphrase: [输入用户的登录口令]

现在,使用用户的登录口令可以从文件/root/.ecryptfs/wrapped-passphrase中得到 eCryptfs 的加密口令。即使加密口令很强,如果登录口令弱的话,文件信息还是会泄漏的。所以,得选个强的登录口令,不然就不要玩登录时自动挂载加密 $HOME 了。

或者,你也可以玩点有趣的,把这个wrapped-passphrase文件放在 U 盘里,只留下一个指向 U 盘里的此文件的软链接。然后配置好 U 盘自动挂载,就做成了个简单的「U 盾」!

好了,现在卸载 $HOME:

# umount /home/user

自动挂载

先把 eCryptfs 的那个在/root下的目录弄回来:

# mv /root/.ecryptfs /home/.ecryptfs/user
# chown -R user:user /home/.ecryptfs/user/.ecryptfs
# ln -s /home/.ecryptfs/user/.ecryptfs /home/user/.ecryptfs

接下来,创建一个挂载用的脚本,暂时叫它/home/profile.sh吧。它将被写到用户登录时的自动执行脚本中,如~/.profile,或者~/.zprofile,如果你用 Zsh 的话。

if [ -r "$HOME/.ecryptfs/auto-mount" ]; then
  grep -qs "$HOME ecryptfs" /proc/mounts
  if [ $? -ne 0 ]; then
    mv $HOME/.Xauthority /tmp 2>/dev/null
    mount -i "$HOME"
    cd "$HOME"
    mv /tmp/.Xauthority $HOME 2>/dev/null
    (
      systemctl --user daemon-reload
      systemctl --user default
    ) &
  fi
fi

注意到这里加入了对~/.Xauthority文件的处理,不然从图形界面登录时,执行挂载命令后,会因授权文件不见了而失败。之前把 $HOME 的权限设置成700也是为了这个。单纯地允许写~/.Xauthority不行,因为 xauth 需要创建临时文件以防止此文件同时被多个进程修改。

现在,我们需要在用户登录时自动 unwrap 之前创建的那个wrapped-passphrase文件。在/etc/pam.d/login中添加几行(注意顺序):

#%PAM-1.0
#...
auth                required        pam_unix.so nullok
auth                required        pam_ecryptfs.so unwrap
#...
password            required        pam_ecryptfs.so
#password           required        pam_unix.so sha512 shadow use_authtok
#...

好了,我们先手动试试:

# su user
$ ecryptfs-insert-wrapped-passphrase-into-keyring /home/user/.ecryptfs/wrapped-passphrase
Passphrase: [输入用户密码]
$ mount -i /home/user

如果正确挂载的话,接下来就可以开始建设你的新 $HOME 了,比如把你以前的各种文件复制过去,等等。注意不要在加密的目录内进行 BT 下载哦。你可以建立个/home/.ecryptfs/user/public目录然后软链接到 $HOME 内来使用。

我同时还修改了/etc/pam.d/slim,似乎这样才能在使用 slim 登录时也有效。

呃,还没有结束呢。得把之前的/home/profile.sh文件弄进来。这里演示时只是创建了一个新的.profile文件。如果你已经有了此文件的话,一定不要将其加密,而要将其与此挂载脚本合并。它只能不加密,否则挂载后会出现两个.profile(一个加密了的,一个未加密、passthrough 来的),从而导致一些问题。

# umount /home/user
# chmod 600 /home/profile.sh
# chown user:user /home/profile.sh
# mv /home/profile.sh /home/.ecryptfs/user/private/.profile
# ln -s /home/.ecryptfs/user/private/.profile /home/user/.profile

好了,到此一切结束。

Category: Linux | Tags: linux 安全
1
10
2012
0

GM 脚本:MediaWiki 脚注 tooltip

MediaWiki 使用脚注插件后就多了脚注功能。可无奈这插件把网页当成纸质书了,脚注得点击跳转后才能看到内容。我不胜其烦,遂作此脚本。只对我自己的 wiki 和英文维基百科启用了,因为另一个常去的 MediaWiki 站点——中文维基百科有个导航Popup小工具更好用。我还是一如既往地没有使用 jQuery。

// ==UserScript==
// @name           MediaWiki 脚注 tip
// @namespace      http://lilydjwg.is-programmer.com/
// @include        http://localhost/wiki/*
// @include        https://en.wikipedia.org/wiki/*
// ==/UserScript==

var showTip = function(evt){
  var el = evt.target;
  var left = el.offsetLeft;
  var top = el.offsetTop;
  var tip = document.getElementById('gm-tip');
  //not el.href here; we need the original one
  var tipTextEl = document.getElementById(el.getAttribute('href').substring(1));
  tip.innerHTML = tipTextEl.textContent.substring(2);
  tip.style.top = (top+5) + 'px';
  tip.style.left = (left+25) + 'px';
  tip.style.display = 'block';
};

var hideTip = function(){
  var el = document.getElementById('gm-tip');
  if(el){
    el.style.display = "none";
  }
};

var cites = document.querySelectorAll('.reference > a');
// var cites = document.querySelectorAll('a[href^="#cite_note-"]');
for(var i=0, len=cites.length; i<len; i++){
  cites[i].addEventListener("mouseover", showTip, false);
  cites[i].addEventListener("mouseout", hideTip, false);
}

var setup = function(){
  el = document.createElement('div');
  el.setAttribute('id', 'gm-tip');
  el.style.display = 'none';
  el.style.position = 'absolute';
  el.style.zIndex = '100';
  el.style.border = '1px #1e90ff solid';
  el.style.backgroundColor = 'rgba(115, 201, 230, 0.75)';
  el.style.padding = '0.2em 0.5em';
  var parentEl = cites[0].offsetParent;
  parentEl.appendChild(el);
};
if(cites.length > 0){
  setup();
}
1
9
2012
5

一个 Python 调试函数

Python 有个code模块,可以在程序中开个 REPL 交互命令行,就像 Python 解释器的交互执行一样,调试时非常方便。为了偷懒,我又把它包装了下,写下了repl函数(on github):

def repl(local, histfile=None, banner=None):
  import readline
  import rlcompleter
  readline.parse_and_bind('tab: complete')
  if histfile is not None and os.path.exists(histfile):
    # avoid duplicate reading
    readline.clear_history()
    readline.set_history_length(10000)
    readline.read_history_file(histfile)
  import code
  readline.set_completer(rlcompleter.Completer(local).complete)
  code.interact(local=local, banner=banner)
  if histfile is not None:
    readline.write_history_file(histfile)

之所以要现在把这个函数拿出来,是因为我终于解决了一件让我郁闷很久的问题——补全。历史记录是早就弄好了的,可是补全却经常不给力,补不出东西来,只有少数时候比较正常。这个和 Python 解释器自己的 REPL 不一样。最近在开发 XMPP 群,经常要用到,于是终于去读了rlcompleter.py的代码。还好不长,很快就搞定了:默认使用的是__main__.__dict__这个里边的对象进行补全,而不是globals()。给readline重新设置下补全函数就好了:

readline.set_completer(rlcompleter.Completer(local).complete)
Category: python | Tags: python
1
7
2012
26

Haskell 实战:获取ArchLinux已安装的所有架构相关的软件包名

学而不用则惘。

任务内容

通过读取 pacman 数据库,获取本机已安装软件包中所有架构相关的软件包名。pacman 的数据库中,包描述文件位于/var/lib/pacman/local/*/desc,其中星号部分为软件包名加版本号。该文件中,%NAME%的下一行为软件包名,%ARCH%的下一行为架构,我这里是i686或者any。任务就是找出所有 i686 的软件包名。

任务解析

先写个纯函数,通过一块描述文本(Data.Text)判断这个包是否是架构相关的。类型声明为:

import qualified Data.Text as T
isArchDependent :: T.Text -> Bool

然后看看我们怎么才能办到这点。首先,用T.lines把这「块」文本解析成行的列表。然后我们来找为%ARCH%的这一行。怎么找呢,把前边的行丢掉好了:

(dropWhile (/= archstart)) . T.lines
  where archstart = T.pack "%ARCH%"

现在列表的第二项就是我们要的架构类别。先取两行,最后一行就是了:

last . (take 2) . (dropWhile (/= archstart)) . T.lines

然后做比较,得到最终的结果:

isArchDependent = (/= anyarch) . last . (take 2) . (dropWhile (/= archstart)) . T.lines
                  where archstart = T.pack "%ARCH%"
                        anyarch = T.pack "any"

知道一个包是不是我们要的了,但我们还不知道它的名字。此信息我可以肯定在第二行,就不慢慢 drop 了:

getPackageName :: T.Text -> T.Text
getPackageName = last . (take 2) . T.lines

再来个筛选函数,把将要显示的包描述信息找出来:

filterArchDependent :: [T.Text] -> [T.Text]
filterArchDependent = filter isArchDependent

接下来,是程序中「不纯」的部分。我们需要列出目录/var/lib/pacman/local下的所有目录,然后读取其中的desc文件。

getPackagePaths :: IO [FilePath]
getPackagePaths = (filter ((/= '.') . head)) `fmap` getDirectoryContents "."

getPackageDesc :: FilePath -> IO T.Text
getPackageDesc = TIO.readFile . (++ "/desc")

最后,把以上这些函数组合起来:

topDir = "/var/lib/pacman/local"

main = do
  setCurrentDirectory topDir
  getPackagePaths >>= mapM getPackageDesc >>= ((mapM TIO.putStrLn) . (map getPackageName) . filterArchDependent)

首先为了避免一大堆的路径拼接,进入topDir里边来。然后(main的第三行)写到:获取所有软件包的路径;对于每个路径,获取对应软件包的描述信息并处理;怎么处理呢?先过滤filterArchDependent,再逐个获取包名,最后把它打印出来。

代码

完整的代码如下:

import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import System.Directory (getDirectoryContents, setCurrentDirectory)
import Control.Monad

isArchDependent :: T.Text -> Bool
isArchDependent = (/= anyarch) . last . (take 2) . (dropWhile (/= archstart)) . T.lines
                  where archstart = T.pack "%ARCH%"
                        anyarch = T.pack "any"

filterArchDependent :: [T.Text] -> [T.Text]
filterArchDependent = filter isArchDependent

getPackageName :: T.Text -> T.Text
getPackageName = last . (take 2) . T.lines

topDir = "/var/lib/pacman/local"

getPackagePaths :: IO [FilePath]
getPackagePaths = (filter ((/= '.') . head)) `fmap` getDirectoryContents "."

getPackageDesc :: FilePath -> IO T.Text
getPackageDesc = TIO.readFile . (++ "/desc")

main = do
  setCurrentDirectory topDir
  getPackagePaths >>= mapM getPackageDesc >>= ((mapM TIO.putStrLn) . (map getPackageName) . filterArchDependent)

性能分析

我使用这个 Perl 脚本来计时,跑 20 次取平均时间。Shell 算起算术来太麻烦了 :-(

#!/usr/bin/perl
 
use Time::HiRes qw(gettimeofday);
 
sub gettime {
  my ($sec, $usec) = gettimeofday;
  $sec * 1000_100 + $usec;
}
 
my $times = 20;
my $start = gettime;
for(my $var = 0; $var < $times; $var++){
  `$ARGV[0]`;
}
my $end = gettime;
printf "%lfus\n", ($end - $start) / $times;

作为对照的是个 Python 脚本:

#!/usr/bin/env python3

import os

topDir = "/var/lib/pacman/local"

def checkPackage(file):
  for l in open(file):
    l = l.rstrip()
    if l == '%NAME%':
      next = 'name'
    elif l == '%ARCH%':
      next = 'arch'
    else:
      if next == 'name':
        name = l
      elif next == 'arch':
        return name, l != 'any'
      next = ''

def main():
  for name in os.listdir(topDir):
    if name.startswith('.'):
      continue
    file = '%s/%s/desc' % (topDir, name)
    name, show = checkPackage(file)
    if show:
      print(name)

if __name__ == '__main__':
  main()

这两个脚本长度都差不多,但效率相差挺显著的:

>>> ~tmp/t.pl './packagestat > /dev/null'
86055.100000us
>>> ~tmp/t.pl './packagestat.py > /dev/null'
248090.450000us

花絮

最开始,我用的是Data.Text.LazyData.Text.Lazy.IO这个包里的 Lazy 文本类型,结果是——

>>> ./packagestat
packagestat: glpng-1.45-4/desc: openFile: resource exhausted (Too many open files)

评论

写完这两个脚本,我体会到了Real World Haskell里说的,Even with years of experience, we remain astonished and pleased by how often our Haskell programs simply work on the first try, once we fix those compilation errors. Haskell 程序基本上编译通过后就能正确运行——只是要先修正各种编译错误。Python 那个跑了几遍才得到正确的结果。不过我觉得,除了 GHC 的强大之外,编写逻辑简单、没有状态变量也是正确率高的重要原因之一。

疑问

如果我想同时统计这些软件包的总大小(包描述信息里有),怎么才能只读一遍这些文件就同时做到这两件事呢?

Category: Haskell | Tags: Haskell

| Theme: Aeros 2.0 by TheBuckmaker.com