From 62d8b40985741ca2358f4de3b0fd97fd02b49625 Mon Sep 17 00:00:00 2001
From: Kyle Keen <keenerd@gmail.com>
Date: Thu, 14 Nov 2013 21:46:36 -0500
Subject: [PATCH] Add py_mtime rule

Signed-off-by: Kyle Keen <keenerd@gmail.com>
---
 Namcap/rules/__init__.py |   1 +
 Namcap/rules/py_mtime.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++
 Namcap/util.py           |  28 ++++++++++
 namcap-tags              |   3 ++
 4 files changed, 161 insertions(+)
 create mode 100644 Namcap/rules/py_mtime.py

diff --git a/Namcap/rules/__init__.py b/Namcap/rules/__init__.py
index f7780d2..8dc4e68 100644
--- a/Namcap/rules/__init__.py
+++ b/Namcap/rules/__init__.py
@@ -42,6 +42,7 @@ from . import (
   missingbackups,
   perllocal,
   permissions,
+  py_mtime,
   rpath,
   scrollkeeper,
   shebangdepends,
diff --git a/Namcap/rules/py_mtime.py b/Namcap/rules/py_mtime.py
new file mode 100644
index 0000000..aaff238
--- /dev/null
+++ b/Namcap/rules/py_mtime.py
@@ -0,0 +1,129 @@
+#
+# namcap rules - py_mtime
+# Copyright (C) 2013 Kyle Keen <keenerd@gmail.com>
+#
+#   This program is free software; you can redistribute it and/or modify
+#   it under the terms of the GNU General Public License as published by
+#   the Free Software Foundation; either version 2 of the License, or
+#   (at your option) any later version.
+#
+#   This program is distributed in the hope that it will be useful,
+#   but WITHOUT ANY WARRANTY; without even the implied warranty of
+#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#   GNU General Public License for more details.
+#
+#   You should have received a copy of the GNU General Public License
+#   along with this program; if not, write to the Free Software
+#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+"""
+Check for py timestamps that are ahead of pyc/pyo timestamps
+"""
+
+import os
+from Namcap.util import load_mtree
+from Namcap.ruleclass import *
+
+def _quick_filter(names):
+	"can this package be skipped outright"
+	if not names:
+		return True
+	found_py  = any(n.endswith('.py')  for n in names)
+	found_pyc = any(n.endswith('.pyc') for n in names)
+	found_pyo = any(n.endswith('.pyo') for n in names)
+	if found_py and found_pyc:
+		return False
+	if found_py and found_pyo:
+		return False
+	return True
+
+def _tar_timestamps(tar):
+	"takes a tar object"
+	return dict((m.name, m.mtime) for m in tar.getmembers())
+
+def _mtree_timestamps(tar):
+	"takes a tar object"
+	return dict((h, a['time']) for h,a in load_mtree(tar) if 'time' in a)
+
+def _generic_timestamps(tar):
+	"works for mtree and tar"
+	if '.MTREE' in tar.getnames():
+		return _mtree_timestamps(tar)
+	return _tar_timestamps(tar)
+
+def _try_mtree(tar):
+	"returns True if good, False if bad, None if N/A"
+	if '.MTREE' not in tar.getnames():
+		return None
+	stamps = _mtree_timestamps(tar)
+	if _quick_filter(stamps.keys()):
+		return True
+	return not _mtime_filter(stamps)
+
+def _try_tar(tar):
+	"returns True if good, False if bad"
+	names = tar.getnames()
+	if _quick_filter(names):
+		return True
+	mtimes = _tar_timestamps(tar)
+	return not _mtime_filter(mtimes)
+
+def _split_all(path):
+	"like os.path.split but splits every directory"
+	p2 = path
+	dirs = []
+	while p2 and p2 != '/':
+		p2,p3 = os.path.split(p2)
+		dirs.insert(0, p3)
+	#dirs.insert(0, '/')
+	return dirs
+
+def _source_py(path):
+	"given a pyc/pyo, return the source path"
+	if not path.endswith('.pyc') and not path.endswith('.pyo'):
+		return None
+	path = path[:-1]
+	# handle py2
+	if '__pycache__' not in path:
+		return path
+	# handle py3
+	splitup = _split_all(path)
+	if splitup[-2] != '__pycache__':
+		return None
+	splitup.pop(-2)
+	f = splitup[-1]
+	f = f.split('.')
+	f.pop(-2)
+	splitup[-1] = '.'.join(f)
+	return os.path.join(*splitup)
+
+def _mtime_filter(mtimes):
+	"return list of bad py file names"
+	bad = []
+	for name, mt2 in mtimes.items():
+		if not name.endswith('.pyc') and not name.endswith('.pyo'):
+			continue
+		source_name = _source_py(name)
+		if source_name not in mtimes:
+			continue
+		mt1 = mtimes[source_name]
+		if mt1 > mt2:
+			bad.append(source_name)
+	return bad
+
+class package(TarballRule):
+	name = "py_mtime"
+	description = "Check for py timestamps that are ahead of pyc/pyo timestamps"
+	def analyze(self, pkginfo, tar):
+		mtree_status = _try_mtree(tar)
+		tar_status = _try_tar(tar)
+		if mtree_status == False and tar_status:
+			# mtree only
+			self.warning = [('py-mtime-mtree-warning', ())]
+		elif not tar_status:
+			# tar or both
+			self.errors = [('py-mtime-tar-error', ())]
+		self.infos = [('py-mtime-file-name %s', f[1:]) for f in _mtime_filter(_generic_timestamps(tar))]
+
+# vim: set ts=4 sw=4 noet:
diff --git a/Namcap/util.py b/Namcap/util.py
index 21d7163..0613202 100644
--- a/Namcap/util.py
+++ b/Namcap/util.py
@@ -20,6 +20,7 @@
 import os
 import re
 import stat
+import gzip
 
 def _read_carefully(path, readcall):
 	if not os.path.isfile(path):
@@ -77,4 +78,31 @@ def script_type(path):
 
 clean_filename = lambda s: re.search(r"/tmp/namcap\.[0-9]*/(.*)", s).group(1)
 
+def _mtree_line(line):
+	"returns head, {key:value}"
+	# todo, un-hex the escaped chars
+	head,_,kvs = line.partition(' ')
+	kvs = dict(kv.split('=') for kv in kvs.split(' '))
+	return head, kvs
+
+def load_mtree(tar):
+	"takes a tar object, returns (path, {attributes})"
+	if '.MTREE' not in tar.getnames():
+		raise StopIteration
+	zfile = tar.extractfile('.MTREE')
+	text = gzip.open(zfile).read().decode("utf-8")
+	defaults = {}
+	for line in text.split('\n'):
+		if not line:
+			continue
+		if line.startswith('#'):
+			continue
+		head, kvs = _mtree_line(line)
+		if head == '/set':
+			defaults = kvs
+		attr = {}
+		attr.update(defaults)
+		attr.update(kvs)
+		yield head, attr
+
 # vim: set ts=4 sw=4 noet:
diff --git a/namcap-tags b/namcap-tags
index d638478..8b67330 100644
--- a/namcap-tags
+++ b/namcap-tags
@@ -67,6 +67,9 @@ perllocal-pod-present %s :: perllocal.pod found in %s.
 pkgname-in-description :: Description should not contain the package name.
 potential-non-fhs-info-page %s :: Potential non-FHS info page (%s) found.
 potential-non-fhs-man-page %s :: Potential non-FHS man page (%s) found.
+py-mtime-mtree-warning :: Found .py file newer (sub-second) than associated .pyc/pyo.
+py-mtime-tar-error :: Found .py file newer than associated .pyc/pyo.
+py-mtime-file-name %s :: Python script (%s) is newer than associated .pyc/pyo.
 script-link-detected %s in %s :: Script link detected (%s) in file %s
 scrollkeeper-dir-exists %s :: Scrollkeeper directory exists (%s). Remember to not run scrollkeeper till post_{install,upgrade,remove}.
 site-ruby :: Found usr/lib/ruby/site_ruby in package, usr/lib/ruby/vendor_ruby should be used instead.
-- 
2.0.1

